タプル型 T において、なぜ T[number] はUnion型になるのかに関する考察

6 min read読了の目安(約5500字

始め

最近 type-challenges を頑張って挑戦しています(難しくて全然とけてない)。

この間は以下のような書き方を覚えまして、

type Colors = ["white", "red", "black", "purple"]
type ColorsUnion = Colors[number] // "white" | "red" | "black" | "purple"

え??なんで??」と思いました。その「なんで?」を探っていきたいと思います。

1. タプル型

まずは基本のタプル型について Typescript 公式ドキュメントに説明とサンプルコートを読みました。

A tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.

type StringNumberPair = [string, number];

ざっくり言うと「要素の数とどの要素がどの位置(index)にあるかが決まってる配列型の一種」ですね。

ここでスクロールをもっと下に回したら大事な部分があります。

simple tuple types like these are equivalent to types which are versions of Arrays that declare properties for specific indexes, and that declare length with a numeric literal type.
このような単純なタプル型は、特定のインデックスのプロパティを宣言し、numeric literal type でlengthを宣言するArrayたちのバージョンと同じです。

interface StringNumberPair {
  // specialized properties
  length: 2;
  0: string;
  1: number;

  // Other 'Array<string | number>' members...
  slice(start?: number, end?: number): Array<string | number>;
}

「タプル型はこういうinterfaceと同じだよー」と書いてあります。なるほどなるほど。

2. T[K]

次はT[K]です。これはTに対してKでアクセスして得られる型を返す機能で、2016年11月に Static types for dynamically named properties というPR名で追加されました(結構前ですね)。

理解のため該当PRからサンプルコートの一部を借りてきました。

interface Thing {
    name: string;
    width: number;
    height: number;
    inStock: boolean;
}

type P1 = Thing["name"];  // string
type P2 = Thing["width" | "height"];  // number
type P3 = Thing["name" | "inStock"];  // string | boolean

例で見たらわかりやすいです。サンプルコードと一緒に一言説明も載せられています。

Indexed access types of the form T[K], where T is some type and K is a type that is assignable to keyof T (or assignable to number if T contains a numeric index signature).

お?? number?? numeric index signature?? それっぽいものが出てきました。

3. index signature

新しい手がかりである numeric index signature を調べたら、以前の Typescript Handbook からこういう説明を見つけました。

we can also describe types that we can “index into” like a[10], or ageMap["daniel"]. Indexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing.

つまり、index signature というのはインデックシングする時に使う型と、それに相応する返り値の型を記述こととも言えます。

つい先見たサンプルコードの一部をもう一度見てみましょう。

interface Thing {
    name: string;
    width: number;
    height: number;
    inStock: boolean;
}

type P1 = Thing["name"];  // string

これ例だったらインデックシングする時に使う型は"name"、返り値の型はstringになりますね。

ちなみにインデックシングする時に使える型はstringnumberの2つだけです。

  • stringを使ったら? → string index signature
  • numberを使ったら? → numeric index signature

なるほどなるほど。

4. 推論

必要な材料は揃ったと思いますので、最初にお見せしたサンプルコートで推論を始めます。

type Colors = ["white", "red", "black", "purple"]

まず、Colorsはタプル型ですので以下のinterfaceと同じだと考えられます。

interface Colors {
  length: 4;
  0: "white";
  1: "red";
  2: "black";
  3: "purple";
}

このColors0123という numeric index signature を含めているため、numberでインデックシングできます。

type ColorsUnion = Colors[number] // "white" | "red" | "black" | "purple"

そしてnumber型である0123がそれぞれの返り値を返してこういう変換になるのではないかと!私は思いました!

+) ちなみに同じ原理でタプル型のlengthを取り出すこともできます。

type ColorsLength = Colors["length"] // 4

5. T[string]?

実は最初に「え?こういうのできるの?じゃ、Colors[string]とかもできる?」と思って試しました。

type Colors = ["white", "red", "black", "purple"]
type ColorsString = Colors[string] //Type 'Colors' has no matching index signature for type 'string'

そしてさっそく「stringに当てはまる index signature なんてないよ」と怒られました。確かに、タプル型をinterfaceに書き換えてもstringindex signature はなかったから当たり前…うん?

あれ?lengthstringじゃん??

と思い、また調べました。ちょうど stackoverflow に私と同じ質問をした人がいたので(What's the T[number] mean in typescript code?)、そこのコメントを参考にしました。

You can’t use T[string] because Array doesn’t have a string index signature. You’re allowed to use those, but Array doesn’t. Since it doesn’t have a string index signature, T[string] isn’t legal. You can use T['length'], though, since Array does have a property with that particularly string. Using string or number refers to any string or number—which requires an index signature.

要約

  • Arraystringindex signature を持っていない
  • T['length']が使えるのはlengthという特定の文字列を持ってるため
  • stringnumberを使うことはすべての文字列、数字を意味するし、そのためには index signature が必要

なるほどなるほど。T[string]ですべての string index signature にアクセスできるようになるためには index signature が必要ということですね。このコメントを作成した人もそう言ってました。

interface Dictionary<Value> {
    [key: string]: Value;
}

With this, we can use T[string] when T is some Dictionary—and T[string] will be Value.

そして配列の場合はlengthという特定の文字列は持ってるけど、index signature がないからT[string]はだめだったというわけででしょう。

終わり

index signature の説明は以前の Typescript Handbook を参考しましたが、「This page has been deprecated 」と表示される上に最近のTypescript Handbook には載ってないためあってるか不安です。もし間違ってる部分あったらおしえてください!