TypeScriptのタプルは、要素数が決まっていて、位置ごとに型を持つ配列です。[string, number] は、最初が文字列で次が数値の、ちょうど2つの要素を表します。値が並ぶ順に、型を角かっこの中に書きます。
実行時のタプルはただのJavaScriptの配列です。タプルが加えるもの(固定の長さと各位置の型)はすべてコンパイラーがチェックし、そのあと消去されます。
タプルの構文
| タプル型 | 受け付ける値 | length の型 |
|---|---|---|
[string, number] | 文字列のあとに数値、ちょうど2つ | 2 |
[x: number, y: number] | 同じもの。読みやすさのためのラベル付き | 2 |
[number, number, number?] | 2つか3つの数値 | 2 | 3 |
[string, ...number[]] | 文字列のあとに任意の個数の数値 | number |
[...string[], number] | 任意の個数の文字列のあとに数値 | number |
readonly [number, number] | 変更できないペア | 2 |
[] | 空の配列だけ | 0 |
それぞれの書き方は以下で説明します。length の型に注目してください。固定のタプルではリテラル型なので、コンパイラーは pair.length がちょうど 2 だと知っています。
コンパイラーがチェックすること
タプル型は要素の数、順序、各位置の型を固定します。どれかを間違えるとコンパイルエラーです:
index.ts(2,7): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
Source has 1 element(s) but target requires 2.
index.ts(3,36): error TS2322: Type 'number' is not assignable to type 'string'.
index.ts(3,40): error TS2322: Type 'string' is not assignable to type 'number'.
index.ts(5,16): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
普通の配列では最後のエラーは検出できません。string[] では arr[2] はただの string で、実行時にたまたま undefined になるだけです。
ラベル付きタプル要素
ラベルは各位置の意味を示します。型やインデックスでのアクセスは何も変わりませんが、エディターのホバーやシグネチャのヒントに表示されるので、[number, number] がぐっとわかりやすくなります。
TypeScript 5.2 からは、[first: string, number] のように一部の位置にだけラベルを付けられます。ラベルは読む人のためだけのものです。[x: number, y: number] と [number, number] は同じ型で、互いに代入できます。
省略可能な要素
要素の型のあとに ? を付けると、その位置は省略可能になります。省略可能な要素は必須の要素のあとに置く必要があり、そのたびに length の型が広がります。
省略可能な要素を読むと T | undefined になるので、計算の前に分割代入のデフォルト値(a = 1)かチェックが必要です。
残余要素
残余要素 ...T[] は、T 型の要素が任意の個数並ぶことを表します。末尾、先頭、途中のどこにでも置けますが、1つのタプルにつき1つまでです。
残余要素を持つタプルの length は、サイズが固定でなくなるので number です。固定されたままなのは、型の付いた位置がどこにあるかです。
readonly タプルと as const
readonly [T, U] は push、pop、splice、インデックスへの代入を取り除きます。長さが固定された値は本来こうあるべきです。配列リテラルのあとに as const と書くと、リテラル型の readonly タプルと推論されます。
(typeof SIZES)[number] はタプルを要素の型のユニオンに変えます。このパターンはインデックスアクセス型で説明しています。readonly タプルは変更可能なタプルとして型付けされたパラメーターには渡せないので、読むだけの関数は readonly [number, number] を受け取るようにしましょう。
readonly のチェックはコンパイル時だけです。実行時の配列は凍結されていない(出力のとおり、上の代入は実際に実行されました)ので、実行時にも保証が必要なら Object.freeze を使います。
関数からタプルを返す
複数の値をタプルとして返すのは、React の useState のしくみです(const [value, setValue] = useState(0))。落とし穴は、return の配列リテラルがタプルではなく配列と推論されることです。
index.ts(9,13): error TS2365: Operator '+' cannot be applied to types 'number | (() => number)' and 'number'.
index.ts(10,1): error TS2349: This expression is not callable.
Not all constituents of type 'number | (() => number)' are callable.
Type 'number' has no call signatures.
この関数は (number | (() => number))[] を返すので、分割代入した2つの名前はどちらもユニオン型になります。直し方は2つあります。戻り値の型を書くか、as const を付けます。
戻り値をタプルにすると、呼び出し側は各部分に好きな名前を付けられます。値が3つ以上ある場合や順序がわかりにくい場合は、代わりにオブジェクトを返しましょう。{ count, increment } なら説明がなくても意味がわかります。
関数のパラメーターとしてのタプル
タプルで型付けした残余パラメーターは、省略可能な引数も含めて引数リスト全体を表します。組み込みのユーティリティ型 Parameters<T> は、この方法で関数のパラメーターを表します。
タプルを呼び出しにスプレッドすると、各引数が位置ごとに型チェックされます。(string | number)[] のスプレッドではこうはいきません。
タプルと配列の違い
配列 (string | number)[] | タプル [string, number] | |
|---|---|---|
| 長さ | 任意 | 固定(または省略可能・残余要素で決まる範囲) |
x[0] の型 | string | number | string |
x[5] の型 | string | number | コンパイルエラー TS2493 |
length の型 | number | 2 |
| 型の順序 | 追跡しない | 追跡する |
| 実行時の値 | JavaScriptの配列 | 同じJavaScriptの配列 |
| 主な用途 | 似た項目のリスト | 小さな固定のまとまり: ペア、座標、[key, value]、複数の戻り値 |
タプルは組み込みの型にも登場します。Object.entries(obj) は [string, T][] を返し、Map は [key, value] のタプルから作られます:
落とし穴がひとつあります。変更可能なタプルは配列のメソッドをすべて持っているので、[string, number] に対して pair.push(3) もコンパイルが通り、型は2つと言っているのに要素が3つの配列が黙って作られます。タプルを readonly で宣言すればこの穴はふさがります。また、型は消去されるので、プログラムの外から来るデータ(JSON や API)は実行時にタプル型でチェックされません。信頼する前に長さと要素の型を検証してください。
可変長タプル型
タプル型はほかのタプル型を [...T, ...U] のようにスプレッドできます。ジェネリクスと組み合わせると、すべての位置を保ったまま連結したり先頭に追加したりする関数に型を付けられます:
ライブラリの型もタプルの推論に頼っています。Promise.all([fetchUser(), fetchPosts()]) は、入力の Promise ごとにひとつの型を持つタプルに解決されます。
よくある質問
TypeScriptのタプルとは何ですか?
長さが決まっていて、位置ごとに型を持つ配列型です。[string, number] は、文字列のあとに数値が続くちょうど2つの要素です。実行時には普通のJavaScriptの配列で、長さと位置ごとの型はコンパイル時にだけチェックされます。
TypeScriptのタプルと配列の違いは何ですか?
(string | number)[] のような配列型は長さが自由で、すべての要素が同じ(ユニオンの)型なので、arr[0] は string | number です。[string, number] のようなタプルは長さがわかっていて、t[0] は string、t[1] は number、t[2] はコンパイルエラーになります。
TypeScriptで関数からタプルを返すには?
戻り値の型を function f(): [number, string] のように書くか、return する式の最後に as const を付けて readonly タプルにします。どちらもしないと、return [count, setCount] は (number | (() => void))[] のようなユニオンの配列と推論され、分割代入した値もユニオン型になります。
ラベル付きタプル要素とは何ですか?
位置に付ける名前で、[name: string, age: number] のように書きます。型もアクセス方法も変わりません(t[0] のまま)が、エディターのホバーや、パラメーターをタプルで型付けした関数のパラメーターヒントに表示されます。省略可能な要素や残余要素にもラベルを付けられます: [x: number, y?: number]、[head: string, ...rest: number[]]。
TypeScriptのタプルに push できますか?
変更可能なタプルならできます。タプルは配列のメソッドを受け継いでいるので、固定の長さが崩れても push はコンパイルが通ります。タプルを readonly で宣言する(または as const で作る)と、push、pop、インデックスへの代入がコンパイルエラーになります。