タプル型 T において、なぜ T[number] はUnion型になるのかに関する考察
始め
最近 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
Array
s that declare properties for specific indexes, and that declarelength
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]
, whereT
is some type andK
is a type that is assignable tokeyof T
(or assignable tonumber
ifT
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]
, orageMap["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
になりますね。
ちなみにインデックシングする時に使える型はstring
とnumber
の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";
}
このColors
は0
、1
、2
、3
という numeric index signature を含めているため、number
でインデックシングできます。
type ColorsUnion = Colors[number] // "white" | "red" | "black" | "purple"
そしてnumber
型である0
、1
、2
、3
がそれぞれの返り値を返してこういう変換になるのではないかと!私は思いました!
+) ちなみに同じ原理でタプル型の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
に書き換えてもstring
の index signature はなかったから当たり前…うん?
あれ?length
はstring
じゃん??
と思い、また調べました。ちょうど stackoverflow に私と同じ質問をした人がいたので(What's the T[number] mean in typescript code?)、そこのコメントを参考にしました。
You can’t use
T[string]
becauseArray
doesn’t have a string index signature. You’re allowed to use those, butArray
doesn’t. Since it doesn’t have a string index signature,T[string]
isn’t legal. You can useT['length']
, though, sinceArray
does have a property with that particularly string. Usingstring
ornumber
refers to any string or number—which requires an index signature.
要約
-
Array
はstring
の index signature を持っていない -
T['length']
が使えるのはlength
という特定の文字列を持ってるため -
string
やnumber
を使うことはすべての文字列、数字を意味するし、そのためには index signature が必要
なるほどなるほど。T[string]
ですべての string index signature にアクセスできるようになるためには index signature が必要ということですね。このコメントを作成した人もそう言ってました。
interface Dictionary<Value> {
[key: string]: Value;
}
With this, we can use
T[string]
whenT
is someDictionary
—andT[string]
will beValue
.
そして配列の場合はlength
という特定の文字列は持ってるけど、index signature がないからT[string]
はだめだったというわけででしょう。
終わり
index signature の説明は以前の Typescript Handbook を参考しましたが、「This page has been deprecated 」と表示される上に最近のTypescript Handbook には載ってないためあってるか不安です。もし間違ってる部分あったらおしえてください!
Discussion