1. ホーム
  2. TypeScript

【TypeScript】タプル型(Tuple)の使い方|要素の型と数を固定した配列を作る

Share

TypeScript で配列の型を書くとき、ふつうは string[]number[] のように「同じ型の要素が並んだもの」を表します。ですが実際には、「1番目は文字列、2番目は数値」というように位置ごとに型が決まっていて、要素の個数も固定したい配列が必要になることがあります。これを表現するのがタプル型(Tuple)です。この記事では、タプル型の基本形から、読みやすさを高めるラベル付きタプル、オプション要素や残余要素(...)、変更を防ぐ readonly、そして useState のように「複数の値をまとめて返す」実践例までを、初心者向けに解説します。

タプル型の基本形

タプル型は、角かっこの中に型を順番に並べて書きます。[string, number] と書けば、「1番目が文字列・2番目が数値で、要素はちょうど2つ」という配列の型になります。通常の配列型 string[] が「文字列がいくつ並んでもよい」のに対し、タプルは順番と個数が固定される点が大きく違います。

tuple-basic.ts
// 1番目は string、2番目は number のタプル
let user: [string, number] = ['佐藤', 28];

console.log(user[0].toUpperCase()); // 0番目は string として扱える
console.log(user[1].toFixed(0));    // 1番目は number として扱える

// user = [28, '佐藤']; // エラー: 型の順番が違う
// user = ['佐藤'];      // エラー: 要素数が足りない

user[0]stringuser[1]number として扱われるため、それぞれの型に合ったメソッドを安全に呼べます。順番を入れ替えたり、要素の数が合わなかったりするとコンパイルエラーになります。通常の配列型との違いを、次の表で整理しておきましょう。

型の書き方意味
string[]文字列がいくつでも並ぶ配列(個数・位置は自由)
[string, number]1番目が文字列・2番目が数値・要素はちょうど2つ
[number, number, number]数値がちょうど3つ(例: RGB の色)

ラベル付きタプルで読みやすくする

[string, number] だけを見ても、それぞれの要素が何を表すのかは分かりません。そこで、各要素に名前(ラベル)を付けられます。これをラベル付きタプルと呼びます。ラベルは型の意味を説明するためのもので、実行時の動作には影響しませんが、コードを読む人やエディタの補完にとって大きな助けになります。

labeled-tuple.ts
// 各要素に name・age というラベルを付ける
type User = [name: string, age: number];

const user: User = ['鈴木', 34];

// 緯度・経度のように意味のある座標にも便利
type LatLng = [lat: number, lng: number];
const tokyo: LatLng = [35.68, 139.76];

[name: string, age: number] のように書くと、エディタでタプルにマウスを乗せたときにラベルが表示され、それぞれの要素が何を意味するのかが一目で分かります。座標や色のように「要素の順番に意味がある」データでは、ラベルを付けておくと勘違いによるミスを減らせます。

オプション要素と残余要素

タプルは要素数を固定するのが基本ですが、「あってもなくてもよい要素」や「後ろにいくつでも続く要素」も表現できます。要素名のあとに ? を付けるとオプション要素(省略可能)になり、...型[] と書くと残余要素(残りをまとめて受ける)になります。

optional-rest.ts
// 3番目は省略可能なオプション要素
type Point = [x: number, y: number, z?: number];
const p2d: Point = [10, 20];      // z を省略できる
const p3d: Point = [10, 20, 30];  // z を付けてもよい

// 先頭は文字列、そのあとは数値がいくつでも続く
type Command = [name: string, ...args: number[]];
const cmd: Command = ['move', 10, 20, 30];

z?: number とすると3番目は省略でき、2次元・3次元の座標を1つの型で扱えます。...args: number[] は「1番目のあとに数値がいくつでも続く」という意味で、コマンド名とその引数のような可変長のデータに向いています。オプション要素は残余要素より前に、残余要素は原則として末尾に置く、という順序のルールがあります。

readonly で変更を防ぐ

タプルは配列なので、そのままでは要素を書き換えたり push で追加したりできてしまいます。中身を変えたくない定数的なデータには、先頭に readonly を付けると安全です。readonly を付けたタプルは、要素への代入や push などの破壊的な操作がコンパイルエラーになります。

readonly-tuple.ts
// 変更できないタプル
const rgb: readonly [number, number, number] = [255, 128, 0];

// rgb[0] = 0;   // エラー: readonly なので代入できない
// rgb.push(255); // エラー: readonly なので push できない

console.log(rgb[0]); // 読み取りはできる: 255

色や設定値のように「定義したら変えない」データには readonly を付けておくと、意図しない書き換えをコンパイル時に防げます。なお、配列リテラルの末尾に as const を付けると、TypeScript が自動的に readonly のタプルとして推論してくれるため、こちらもよく使われます。

関数から複数の値をまとめて返す

タプルがもっとも活躍するのは、関数から複数の値をまとめて返す場面です。React の useState[値, 更新関数] という配列を返し、const [count, setCount] = useState(0) のように受け取るのは、まさにタプルの典型例です。自分の関数でも同じ形を作れます。

tuple-return.ts
// [商, 余り] のタプルを返す
function divmod(a: number, b: number): [number, number] {
  return [Math.floor(a / b), a % b];
}

// 分割代入で名前を付けて受け取れる
const [quotient, remainder] = divmod(17, 5);
console.log(quotient);  // 3
console.log(remainder); // 2

戻り値の型を [number, number] にしておくと、呼び出し側では const [quotient, remainder] = ... のように分割代入で受け取り、それぞれに好きな名前を付けられます。オブジェクトで { quotient, remainder } と返す方法もありますが、受け取る側で自由に名前を決めたいときや、順番が明確なペアを返すときはタプルが便利です。

タプルが崩れてしまうとき

型注釈を書かないと普通の配列になる

const pair = ['佐藤', 28] のように型注釈なしで書くと、TypeScript はこれをタプルではなく (string | number)[] という普通の配列と推論します。そのため pair[0] の型も string | number になり、タプルの利点が失われます。タプルとして扱いたいときは、変数に [string, number] の型注釈を付けるか、リテラルの末尾に as const を付けて固定しましょう。

push などで要素数の保証が失われる

タプルは要素数を固定する型ですが、readonly でなければ push による追加自体はコンパイルエラーになりません。しかし追加した要素にはインデックスでアクセスしても型が付かず、「2要素のはず」という前提が実行時に崩れます。要素数を守りたいタプルは readonly を付けて破壊的操作を禁止しておくと、こうした崩れを防げます。

まとめ

タプル型は、[string, number] のように位置ごとの型と要素数を固定した配列を表す型です。各要素にラベルを付ければ意味が明確になり、? のオプション要素や ...T[] の残余要素で柔軟さも持たせられます。中身を変えたくないときは readonly(や as const)で保護でき、関数から複数の値をまとめて返して分割代入で受け取る、という useState のような使い方が代表的な活躍の場です。ただし型注釈を書かないと普通の配列と推論される点には注意が必要です。「順番と個数に意味がある配列」を扱うときは、タプル型を使って型で守ると、安全で読みやすいコードになります。

参考ページ