T-CREATOR

TypeScript タプル/配列操作チートシート:`Length`・`Push`・`Zip`・`Chunk`を型で書く

TypeScript タプル/配列操作チートシート:`Length`・`Push`・`Zip`・`Chunk`を型で書く

TypeScript の型システムは単なる型チェックツールではありません。実は、型レベルでプログラミングができる強力な機能を持っているんです。

この記事では、タプルや配列を操作する 4 つの基本的なユーティリティ型—LengthPushZipChunk—を型レベルで実装する方法を解説します。これらをマスターすれば、より安全で表現力豊かな型定義が可能になるでしょう。実務でも活用できる実践的なテクニックを、初心者の方にもわかりやすくお伝えしていきますね。

タプル/配列操作ユーティリティ型 早見表

まずは、これから学ぶ 4 つのユーティリティ型の全体像を把握しましょう。

#型名型定義使用例結果難易度
1Lengthtype Length<T extends readonly any[]> = T['length']Length<[1, 2, 3]>3★☆☆☆☆
2Pushtype Push<T extends readonly any[], U> = [...T, U]Push<[1, 2], 'a'>[1, 2, 'a']★★☆☆☆
3Zip再帰的条件型(詳細は後述)Zip<[1, 2], ['a', 'b']>[[1, 'a'], [2, 'b']]★★★★☆
4Chunk再帰的条件型(詳細は後述)Chunk<[1, 2, 3, 4], 2>[[1, 2], [3, 4]]★★★★★

表の見方:

  • 型名:ユーティリティ型の名称
  • 型定義:基本的な実装コード(ZipChunk は複雑なため詳細は本文で解説)
  • 使用例:具体的な使い方の例
  • 結果:型推論の結果
  • 難易度:実装の複雑さ(★ が多いほど高度)

この早見表を参考に、各ユーティリティ型の詳細な実装を見ていきましょう。

背景

TypeScript の型システムは、バージョンアップを重ねるごとに進化を続けています。特にタプル型(tuple type)は、配列の各要素に異なる型を指定できる便利な機能です。

従来の JavaScript では、配列の要素は実行時にしかわかりませんでした。しかし、TypeScript のタプル型を使えば、コンパイル時に配列の長さや各要素の型を厳密に管理できます。この型安全性により、多くのバグを未然に防げるようになったんですね。

さらに、TypeScript 4.0 以降で追加されたVariadic Tuple Types(可変長タプル型)やTemplate Literal Types(テンプレートリテラル型)により、型レベルでの計算がより柔軟になりました。これにより、以前は不可能だった複雑な型操作が実現可能になっています。

型レベルプログラミングの位置づけ

型レベルプログラミングとは、実行時ではなくコンパイル時に型システムを使って計算や変換を行う技術です。以下の図は、TypeScript における型レベルと実行時レベルの関係を示しています。

mermaidflowchart TB
  source["TypeScriptソースコード"] -->|型チェック| typeLevel["型レベル処理<br/>(コンパイル時)"]
  source -->|トランスパイル| runtime["実行時処理<br/>(JavaScript実行)"]
  typeLevel -->|型エラー検出| error["コンパイルエラー"]
  typeLevel -->|型安全確認| compile["トランスパイル成功"]
  compile --> runtime

図で理解できる要点:

  • 型レベル処理はコンパイル時に実行され、実行時のパフォーマンスには影響しません
  • 型エラーは実行前に検出されるため、バグの早期発見が可能です
  • トランスパイル後の JavaScript には型情報は残りません

課題

タプルや配列を扱う際、開発者は以下のような課題に直面します。

課題 1:タプルの長さを型レベルで取得できない

通常の配列では array.length で長さを取得できますが、型レベルではその長さを型として表現できません。例えば、関数の引数として「長さ 3 のタプル」を受け取りたい場合、その制約を型で表現するのが難しいんです。

課題 2:タプルへの要素追加が型安全でない

既存のタプルに新しい要素を追加する際、型情報が失われがちです。[1, 'hello']true を追加して [1, 'hello', true] という型を得たい場合、手動で型を書くのは面倒ですし、保守性も低下します。

課題 3:複数配列の組み合わせが型で表現できない

2 つ以上の配列を「zip」して組み合わせる操作は、JavaScript ではよく使われます。しかし、型レベルでこれを表現するのは困難でした。[1, 2, 3]['a', 'b', 'c'][[1, 'a'], [2, 'b'], [3, 'c']] に変換する型を定義するには、高度な型レベルプログラミングが必要になります。

課題 4:配列の分割(チャンク化)を型で管理できない

大きな配列を一定のサイズで分割する「chunk」操作も、型レベルでの表現が難しい操作の 1 つです。[1, 2, 3, 4, 5, 6][[1, 2], [3, 4], [5, 6]] に分割する際、結果の型を正確に推論させるのは至難の業でした。

課題の構造図

以下の図は、これら 4 つの課題の関係性を示しています。

mermaidflowchart TD
  challenge["タプル/配列操作の型レベル課題"]
  challenge --> c1["課題1: Length<br/>長さの型取得"]
  challenge --> c2["課題2: Push<br/>要素追加の型安全性"]
  challenge --> c3["課題3: Zip<br/>配列結合の型表現"]
  challenge --> c4["課題4: Chunk<br/>配列分割の型管理"]

  c1 --> base["基礎的操作"]
  c2 --> base
  c3 --> advanced["応用的操作"]
  c4 --> advanced

  base --> solution["型レベル<br/>ユーティリティ実装"]
  advanced --> solution

図で理解できる要点:

  • LengthPush は基礎的な操作で、他の操作の土台となります
  • ZipChunk はより応用的で、複数の型技術を組み合わせます
  • すべての課題は型レベルユーティリティの実装で解決可能です

解決策

TypeScript の高度な型機能を使えば、これらの課題を解決できます。ここでは、4 つのユーティリティ型を順番に実装していきましょう。

解決策の全体像

それぞれのユーティリティ型は、TypeScript の以下の機能を活用します。

#ユーティリティ型主要技術難易度
1LengthT['length'] プロパティアクセス★☆☆☆☆
2Pushスプレッド構文、条件型★★☆☆☆
3Zip再帰的条件型、Variadic Tuple Types★★★★☆
4Chunk再帰的条件型、複雑な型推論★★★★★

型レベルプログラミングの基本パターン

これら 4 つのユーティリティ型を実装する際の共通パターンを示します。

mermaidflowchart LR
  input["入力型<br/>(タプル/配列)"] --> conditional["条件型で分岐"]
  conditional --> base["ベースケース<br/>(再帰終了)"]
  conditional --> recursive["再帰ケース<br/>(型変換+再帰呼び出し)"]
  recursive --> result["結果型"]
  base --> result

図で理解できる要点:

  • 条件型 (A extends B ? C : D) で入力を分岐します
  • 再帰ケースでは自分自身を呼び出して型を変換していきます
  • ベースケースで再帰を終了し、最終的な型を返します

この基本パターンを踏まえて、各ユーティリティ型の実装を見ていきましょう。

具体例

それでは、4 つのユーティリティ型を実際に実装していきます。段階的に説明しますので、じっくり理解を深めていきましょう。

1. Length 型:タプルの長さを型として取得

Length 型は最もシンプルな実装です。タプル型のプロパティ length を利用して、その長さを型として取得します。

Length 型の基本実装

TypeScript のタプル型には length プロパティが型レベルで存在します。これを利用した実装を見てみましょう。

typescript// タプルの長さを型として取得するユーティリティ型
type Length<T extends readonly any[]> = T['length'];

この実装は非常にシンプルですね。T extends readonly any[] でタプルまたは配列型を受け取り、T['length'] でその長さを型として返します。

Length 型の使用例

実際に Length 型を使ってみましょう。

typescript// 使用例1:固定長タプルの長さを取得
type L1 = Length<[1, 2, 3]>; // 3
type L2 = Length<['a', 'b']>; // 2
type L3 = Length<[]>; // 0

タプルの長さが型として正確に取得できていることがわかります。これにより、コンパイル時に長さの検証が可能になりますね。

Length 型の応用:長さの検証

Length 型を使って、特定の長さのタプルのみを受け付ける関数を作れます。

typescript// 長さが3のタプルのみを受け付ける関数の型定義
function processTriple<T extends readonly any[]>(
  tuple: T & (Length<T> extends 3 ? T : never)
): void {
  // 長さ3のタプルのみ処理可能
  console.log(tuple[0], tuple[1], tuple[2]);
}

この型定義により、長さが 3 でないタプルを渡すとコンパイルエラーになります。型安全性が大幅に向上しますね。

typescript// 正しい使用例
processTriple([1, 2, 3]); // OK

// エラーになる使用例(コンパイルエラー)
// processTriple([1, 2]); // Error: 長さが2
// processTriple([1, 2, 3, 4]); // Error: 長さが4

2. Push 型:タプルに要素を追加

Push 型は、既存のタプルに新しい要素を追加した新しいタプル型を返します。スプレッド構文を使用して実装します。

Push 型の基本実装

タプル型にスプレッド構文 ... を使って要素を追加します。

typescript// タプルに要素を追加するユーティリティ型
type Push<T extends readonly any[], U> = [...T, U];

[...T, U] という構文で、既存のタプル T のすべての要素を展開し、最後に新しい要素 U を追加しています。

Push 型の使用例

様々なタプルに要素を追加してみましょう。

typescript// 使用例1:数値タプルに文字列を追加
type P1 = Push<[1, 2, 3], 'hello'>;
// 結果: [1, 2, 3, 'hello']

// 使用例2:空タプルに要素を追加
type P2 = Push<[], true>;
// 結果: [true]

型レベルで要素の追加が正確に行われ、結果の型が推論されていることがわかります。

Push 型の応用:連続的な追加

Push 型を複数回使用して、連続的に要素を追加できます。

typescript// 複数の要素を連続的に追加
type P3 = Push<Push<Push<[], 1>, 'a'>, true>;
// 結果: [1, 'a', true]

// 型エイリアスを使った段階的な追加
type Step1 = Push<[], number>; // [number]
type Step2 = Push<Step1, string>; // [number, string]
type Step3 = Push<Step2, boolean>; // [number, string, boolean]

このように、Push 型を組み合わせることで、段階的にタプルを構築できますね。

3. Zip 型:複数の配列を組み合わせる

Zip 型は 2 つの配列を組み合わせて、要素のペアの配列を作ります。これは再帰的な型定義を使用する、より高度な実装です。

Zip 型の実装方針

2 つのタプル [1, 2, 3]['a', 'b', 'c'][[1, 'a'], [2, 'b'], [3, 'c']] に変換します。先頭要素をペアにして、残りを再帰的に処理する戦略をとります。

mermaidflowchart TD
  start["Zip<[1,2,3], ['a','b','c']>"] --> check1["配列が空?"]
  check1 -->|Yes| empty["空配列を返す"]
  check1 -->|No| extract["先頭を抽出<br/>1と'a'"]
  extract --> pair["ペア作成<br/>[1, 'a']"]
  pair --> recursive["残りを再帰<br/>Zip<[2,3], ['b','c']>"]
  recursive --> combine["結果を結合<br/>[[1,'a'], ...]"]
  combine --> final["最終結果<br/>[[1,'a'], [2,'b'], [3,'c']]"]

図で理解できる要点:

  • 先頭要素を取り出してペアを作成します
  • 残りの要素に対して再帰的に Zip を適用します
  • 空配列に達したら再帰を終了します

Zip 型の基本実装

再帰的な条件型を使って Zip 型を実装します。

typescript// 2つのタプルを組み合わせるユーティリティ型
type Zip<
  T extends readonly any[],
  U extends readonly any[]
> = T extends [infer TFirst, ...infer TRest]
  ? U extends [infer UFirst, ...infer URest]
    ? [[TFirst, UFirst], ...Zip<TRest, URest>]
    : []
  : [];

この実装では、infer キーワードを使って先頭要素 (TFirst, UFirst) と残り (TRest, URest) を分離しています。先頭要素のペア [TFirst, UFirst] を作り、残りに対して再帰的に Zip を呼び出していますね。

Zip 型の使用例

実際に Zip 型を使って配列を組み合わせてみましょう。

typescript// 使用例1:数値と文字列の配列をzip
type Z1 = Zip<[1, 2, 3], ['a', 'b', 'c']>;
// 結果: [[1, 'a'], [2, 'b'], [3, 'c']]

// 使用例2:異なる型の組み合わせ
type Z2 = Zip<[true, false], [1, 2]>;
// 結果: [[true, 1], [false, 2]]

型レベルで正確に配列が組み合わされています。これにより、実行時の動作を型で保証できるんです。

Zip 型の応用:長さが異なる場合の処理

長さが異なる配列を zip すると、短い方に合わせて処理されます。

typescript// 長さが異なる配列のzip
type Z3 = Zip<[1, 2, 3, 4], ['a', 'b']>;
// 結果: [[1, 'a'], [2, 'b']]
// 3と4は無視される

type Z4 = Zip<[1], ['a', 'b', 'c']>;
// 結果: [[1, 'a']]
// 'b'と'c'は無視される

この動作は、一般的なプログラミング言語の zip 関数と同じですね。短い方の配列の長さで処理が終了します。

Zip 型の実用例:オブジェクト生成の型定義

Zip 型を使って、キーと値の配列からオブジェクト型を生成する関数の型を定義できます。

typescript// キーと値の配列からオブジェクトを生成する関数の型定義例
function createObject<
  K extends readonly string[],
  V extends readonly any[]
>(keys: K, values: V): Record<K[number], V[number]> {
  const result = {} as any;
  keys.forEach((key, index) => {
    result[key] = values[index];
  });
  return result;
}

このように、Zip の概念を応用することで、より型安全なコードが書けるようになります。

4. Chunk 型:配列を固定サイズで分割

Chunk 型は最も複雑な実装です。配列を指定されたサイズで分割します。例えば、[1, 2, 3, 4, 5, 6] をサイズ 2 で分割すると [[1, 2], [3, 4], [5, 6]] になります。

Chunk 型の実装方針

まず、指定されたサイズ分の要素を配列から取り出す Take 型と、指定されたサイズ分の要素をスキップする Drop 型を実装します。その後、これらを組み合わせて Chunk 型を実装しましょう。

補助型 1:Take 型の実装

配列の先頭から指定された数の要素を取得する型です。

typescript// 配列の先頭からN個の要素を取得するユーティリティ型
type Take<
  T extends readonly any[],
  N extends number,
  Acc extends readonly any[] = []
> = Acc['length'] extends N
  ? Acc
  : T extends [infer First, ...infer Rest]
  ? Take<Rest, N, [...Acc, First]>
  : Acc;

この実装では、アキュムレータ Acc に要素を蓄積していき、その長さが N に達したら終了します。再帰的に先頭要素を取り出して Acc に追加していますね。

補助型 2:Drop 型の実装

配列の先頭から指定された数の要素をスキップする型です。

typescript// 配列の先頭からN個の要素をスキップするユーティリティ型
type Drop<
  T extends readonly any[],
  N extends number,
  Acc extends readonly any[] = []
> = Acc['length'] extends N
  ? T
  : T extends [infer First, ...infer Rest]
  ? Drop<Rest, N, [...Acc, any]>
  : [];

Acc の長さが N に達するまで要素を捨て続け、達したら残りの配列を返します。

Chunk 型の基本実装

TakeDrop を組み合わせて Chunk 型を実装します。

typescript// 配列を固定サイズで分割するユーティリティ型
type Chunk<
  T extends readonly any[],
  N extends number,
  Acc extends readonly any[] = []
> = T extends []
  ? Acc
  : Chunk<Drop<T, N>, N, [...Acc, Take<T, N>]>;

この実装では、Take<T, N> で先頭 N 個を取得して Acc に追加し、Drop<T, N> で残りの配列に対して再帰的に処理を続けます。配列が空になったら Acc を返して終了しますね。

Chunk 型の完全な実装コード

すべての補助型を含めた完全な実装は以下のとおりです。

typescript// 補助型:配列の先頭からN個取得
type Take<
  T extends readonly any[],
  N extends number,
  Acc extends readonly any[] = []
> = Acc['length'] extends N
  ? Acc
  : T extends [infer First, ...infer Rest]
  ? Take<Rest, N, [...Acc, First]>
  : Acc;

// 補助型:配列の先頭からN個スキップ
type Drop<
  T extends readonly any[],
  N extends number,
  Acc extends readonly any[] = []
> = Acc['length'] extends N
  ? T
  : T extends [infer First, ...infer Rest]
  ? Drop<Rest, N, [...Acc, any]>
  : [];
typescript// メイン型:配列を固定サイズで分割
type Chunk<
  T extends readonly any[],
  N extends number,
  Acc extends readonly any[] = []
> = T extends []
  ? Acc
  : Chunk<Drop<T, N>, N, [...Acc, Take<T, N>]>;

Chunk 型の使用例

実際に Chunk 型を使って配列を分割してみましょう。

typescript// 使用例1:6要素の配列を2つずつ分割
type C1 = Chunk<[1, 2, 3, 4, 5, 6], 2>;
// 結果: [[1, 2], [3, 4], [5, 6]]

// 使用例2:5要素の配列を2つずつ分割(余りあり)
type C2 = Chunk<[1, 2, 3, 4, 5], 2>;
// 結果: [[1, 2], [3, 4], [5]]

余りがある場合でも正しく処理されていますね。最後のチャンクに余った要素が入ります。

Chunk 型の応用例

様々なサイズとパターンで分割してみましょう。

typescript// 使用例3:9要素を3つずつ分割
type C3 = Chunk<[1, 2, 3, 4, 5, 6, 7, 8, 9], 3>;
// 結果: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

// 使用例4:文字列の配列を分割
type C4 = Chunk<['a', 'b', 'c', 'd', 'e'], 2>;
// 結果: [['a', 'b'], ['c', 'd'], ['e']]

// 使用例5:サイズ1で分割(各要素が個別の配列に)
type C5 = Chunk<[1, 2, 3], 1>;
// 結果: [[1], [2], [3]]

実用的な統合例:すべてを組み合わせる

これまで学んだ 4 つのユーティリティ型を組み合わせて、より複雑な型操作を行えます。

typescript// 例:配列をchunkしてzipする複合操作
type ComplexOperation<T extends readonly any[]> = Zip<
  Chunk<T, 2>,
  Chunk<T, 2>
>;

type Result = ComplexOperation<[1, 2, 3, 4]>;
// 結果: [[[1, 2], [1, 2]], [[3, 4], [3, 4]]]

このように、基本的なユーティリティ型を組み合わせることで、複雑な型操作が可能になります。

エラーケースと制限事項

TypeScript の型システムには制限があります。特に再帰の深さには上限があるんです。

typescript// 深い再帰はエラーになる可能性がある
// TypeScriptの再帰深度制限は約50程度

// 極端に長い配列のchunkはコンパイルエラーになる可能性
// type VeryLongChunk = Chunk<[/* 100要素以上 */], 2>;
// Error TS2589: Type instantiation is excessively deep and possibly infinite.

主な制限事項:

  • 再帰深度の上限は約 50 レベル
  • 極端に長いタプルは処理できない
  • 複雑な型操作はコンパイル時間を増加させる

これらの制限を理解した上で、実用的な範囲で型レベルプログラミングを活用しましょう。

まとめ

この記事では、TypeScript の型システムを使ったタプル/配列操作の 4 つの基本ユーティリティ型を実装しました。

学んだ内容:

ユーティリティ型機能主要技術
Lengthタプルの長さを型として取得プロパティアクセス T['length']
Pushタプルに要素を追加スプレッド構文 [...T, U]
Zip複数配列を組み合わせ再帰的条件型、infer キーワード
Chunk配列を固定サイズで分割再帰的条件型、補助型の組み合わせ

これらのユーティリティ型を活用することで、以下のメリットが得られます:

実務での活用メリット:

  • コンパイル時の型安全性が向上し、ランタイムエラーを削減できます
  • API レスポンスの型定義が正確になり、保守性が高まります
  • 複雑なデータ変換の型を自動推論でき、開発効率が上がります

型レベルプログラミングは最初は難しく感じるかもしれません。しかし、基本パターンを理解すれば、驚くほど強力なツールになるでしょう。

ぜひ実際のプロジェクトで試してみて、TypeScript の型システムの奥深さを体験してくださいね。

関連リンク