配列の型を要素の型から組み立てる - タプル型と可変長タプル

TypeScript の配列型 string[] は「string の要素がいくつでも入る」という意味で、要素数や各位置の型については何も制約しません。要素の数が決まっていたり、位置ごとに型が違う配列を扱いたい場合、タプル型が必要になります。

配列型とタプル型の違い

// 配列型: 要素数は不定、すべて同じ型
const names: string[] = ["Alice", "Bob", "Charlie"];

// タプル型: 要素数が固定、位置ごとに型を指定
const pair: [string, number] = ["Alice", 30];

配列型は「何個入っているか」に関心がなく、タプル型は「何番目に何の型があるか」まで指定します。pair の 0 番目は必ず string、1 番目は必ず number であり、3 番目の要素を入れようとするとコンパイルエラーになります。

const pair: [string, number] = ["Alice", 30];

pair[0].toUpperCase(); // OK: string のメソッド
pair[1].toFixed(2);    // OK: number のメソッド

pair[2]; // Error: 長さ '2' のタプルにインデックス '2' は存在しない

配列型ではこうした検査が効きません。string[] に対して [100] にアクセスしてもコンパイルエラーにはならず、実行時に undefined が返るだけです。

タプル型が使われる場面

タプル型が最も自然に使われるのは、関数から複数の値を返す場面です。React の useState が代表的な例で、戻り値は [状態, 更新関数] というタプルになっています。

// useState の戻り値型は [S, Dispatch<SetStateAction<S>>]
const [count, setCount] = useState(0);
// count: number, setCount: Dispatch<SetStateAction<number>>

分割代入と組み合わせることで、各変数に正しい型が付きます。これがもし配列型 (number | Dispatch<…>)[] として返されていたら、count にも setCount にもユニオン型が付いてしまい、使うたびに型の絞り込みが必要になります。

自前の関数でも、関連する値をまとめて返すときにタプル型は有用です。

function parseCoordinate(input: string): [number, number] {
  const [lat, lng] = input.split(",").map(Number);
  return [lat, lng];
}

const [lat, lng] = parseCoordinate("35.6762,139.6503");

オブジェクトで返す方法もありますが、タプルのほうが軽量で、分割代入時に好きな変数名を付けられるという利点があります。

ラベル付きタプル

TypeScript 4.0 以降では、タプルの各要素にラベルを付けられます。

type Coordinate = [lat: number, lng: number];
type Range = [min: number, max: number];

ラベルはコンパイル後に消えるため、ランタイムの挙動には影響しません。ただしエディタのツールチップや関数シグネチャで表示されるため、[number, number] だけでは「どっちが何だったか」がわからない場面で可読性が大きく改善されます。

ラベルなし

[number, number] だけでは lat と lng の順序を覚えておく必要がある。ツールチップにも型しか出ない。

ラベル付き

[lat: number, lng: number] なら、ホバーするだけで各位置の意味がわかる。ドキュメントとしても機能する。

オプショナル要素

タプルの末尾の要素は ? を付けてオプショナルにできます。

type LogEntry = [message: string, level?: string, timestamp?: number];

const a: LogEntry = ["server started"];
const b: LogEntry = ["error occurred", "error"];
const c: LogEntry = ["request received", "info", Date.now()];

オプショナル要素は末尾にしか置けません。途中の要素を省略可能にすることはできないため、順序の設計が重要になります。

// Error: オプショナル要素の後に必須要素は置けない
type Bad = [a?: string, b: number];

可変長タプル(Variadic Tuple Types)

TypeScript 4.0 で導入された可変長タプルは、タプルの一部にスプレッド構文を使って「残りの要素」を表現する仕組みです。

type StringAndNumbers = [string, ...number[]];

const a: StringAndNumbers = ["total", 1, 2, 3];
const b: StringAndNumbers = ["sum", 100];
const c: StringAndNumbers = ["empty"];

先頭は必ず string、それ以降は number がいくつでも続けられます。固定部分と可変部分を組み合わせることで、「最初の引数だけ特別」といったパターンを型で表現できます。

スプレッドは先頭や中間にも置けます。

// 末尾が固定
type Padded = [...string[], number];
const x: Padded = ["a", "b", "c", 42];

// 先頭と末尾が固定、中間が可変
type Sandwich = [string, ...number[], string];
const y: Sandwich = ["start", 1, 2, 3, "end"];

ただしスプレッドを複数置けるのは、rest 要素が 1 つだけの場合に限られます。

関数の引数への応用

可変長タプルは関数の引数型と相性がよく、rest パラメータの型を柔軟に定義できます。

function log(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, ...messages);
}

log("info", "server started", "port 3000");

これ自体は普通の rest パラメータですが、可変長タプルを使うと複数の関数の引数型を結合するといった操作が可能になります。

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;

type A = Head<[string, number, boolean]>; // string
type B = Tail<[string, number, boolean]>; // [number, boolean]

infer とスプレッドを組み合わせることで、タプルの先頭や末尾を抽出するユーティリティ型を作れます。これはライブラリの型定義で頻繁に使われるテクニックです。

readonly タプル

タプルに readonly を付けると、要素の変更が禁止されます。

function getRange(): readonly [number, number] {
  return [0, 100];
}

const range = getRange();
range[0] = 10; // Error: readonly プロパティに代入できない

as const でリテラルから作ったタプルも自動的に readonly になります。

const colors = ["red", "green", "blue"] as const;
// 型: readonly ["red", "green", "blue"]

as const なしだと string[] に推論されますが、as const を付けるとリテラル型のタプルになります。各要素が具体的な文字列リテラル型を持つため、ユニオン型の生成元としても使えます。

const colors = ["red", "green", "blue"] as const;
type Color = (typeof colors)[number]; // "red" | "green" | "blue"

タプルとオブジェクト、どちらを使うか

タプルとオブジェクトはどちらも複数の値をまとめる手段ですが、適する場面が異なります。

特徴タプルオブジェクト
要素数少数(2〜3)任意
意味の明確さ位置に依存キー名で明確
分割代入自由な変数名キー名に縛られる

要素が 2〜3 個で、文脈から各位置の意味が明らかな場合(座標、範囲、キーと値のペアなど)はタプルが簡潔です。要素が 4 個以上になったり、各フィールドの役割が名前なしには伝わらない場合はオブジェクトのほうが適しています。React の useState のように、戻り値を分割代入で受け取ることが前提の API ではタプルが定番のパターンになっています。