🎯

【type-challengesに挑戦!】Concat の解答と解説

に公開

はじめに

この記事は、type-challenges の問題を解説するシリーズです。今回は Concat の問題に挑戦します。

問題

JavaScript の Array.concat 関数を TypeScript の型システムで実装します。TypeScript の組み込み型を使わずに実装することが求められます。

Concat<T, U> は、2つの配列型(タプル)を引数として受け取り、両方の要素を左から右の順序で含む新しい配列型を返します。

具体的な使用例:

type Result = Concat<[1], [2]> // 期待される型: [1, 2]

// テストケース
type Test1 = Concat<[], []> // []
type Test2 = Concat<[], [1]> // [1]
type Test3 = Concat<[1, 2], [3, 4]> // [1, 2, 3, 4]
type Test4 = Concat<['1', 2, '3'], [false, boolean, '4']> 
// ['1', 2, '3', false, boolean, '4']

解答

type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U];

解説

ステップ 1: 型パラメータの定義と制約

type Concat<T extends readonly unknown[], U extends readonly unknown[]>

2つの型パラメータを定義しています:

  • T: 1つ目の配列型(タプル)
  • U: 2つ目の配列型(タプル)

ステップ 2: extends readonly unknown[] の制約

T extends readonly unknown[]
U extends readonly unknown[]

この制約が重要な役割を果たします:

  • readonly unknown[]: 任意の型の要素を持つ読み取り専用配列型を表現
  • extends readonly unknown[]: TU が配列型(タプル型)であることを保証し、通常の配列と readonly 配列の両方を受け入れます
    • これにより、配列以外の型を渡すとコンパイルエラーになります

型安全性の例

// ✅ OK: 通常の配列型を渡している
type Valid1 = Concat<[1, 2], [3, 4]>;
type Valid2 = Concat<[], [string, number]>;

// ✅ OK: readonly 配列型も受け入れる
type Valid3 = Concat<readonly [1, 2], readonly [3, 4]>;
type Valid4 = Concat<[1, 2], readonly [3, 4]>; // 混在も可能

// ❌ エラー: 配列型ではない
type Invalid = Concat<string, number>;
// Type 'string' does not satisfy the constraint 'readonly unknown[]'.
// Type 'number' does not satisfy the constraint 'readonly unknown[]'.

ステップ 3: スプレッド構文 [...T, ...U]

[...T, ...U]

TypeScript のバリアディックタプル型(可変長タプル型)を使用しています:

  • ...T: 配列 T のすべての要素を展開
  • ...U: 配列 U のすべての要素を展開
  • [...T, ...U]: 両方の要素を順番に含む新しいタプル型を作成

具体例で見てみましょう

type Example1 = Concat<[1], [2]>;

この場合、以下のように展開されます:

  1. T[1]
  2. U[2]
  3. [...T, ...U][...[1], ...[2]]
  4. 結果: [1, 2]

より複雑な例:

type Example2 = Concat<[1, 2], [3, 4]>;

展開プロセス:

  1. T[1, 2]
  2. U[3, 4]
  3. [...T, ...U][...[1, 2], ...[3, 4]]
  4. 結果: [1, 2, 3, 4]

異なる型が混在する場合:

type Example3 = Concat<['1', 2, '3'], [false, boolean, '4']>;

展開プロセス:

  1. T['1', 2, '3']
  2. U[false, boolean, '4']
  3. [...T, ...U][...['1', 2, '3'], ...[false, boolean, '4']]
  4. 結果: ['1', 2, '3', false, boolean, '4']

各要素の型情報が完全に保持されることに注目してください。

ステップ 4: JavaScript の concat との対応

この型レベルの実装は、JavaScript の Array.concat() メソッドの動作に対応しています:

// JavaScript の実行時の動作
const arr1 = [1, 2];
const arr2 = [3, 4];
const result = arr1.concat(arr2); // [1, 2, 3, 4]

// TypeScript の型レベルでの動作
type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

ステップ 5: バリアディックタプル型の重要性

TypeScript 4.0 で導入されたバリアディックタプル型により、タプル型の要素を柔軟に操作できるようになりました:

// 空の配列の結合
type Empty = Concat<[], []>; // []

// 片方が空の配列
type OneEmpty = Concat<[1, 2], []>; // [1, 2]
type AnotherEmpty = Concat<[], [3, 4]>; // [3, 4]

// 要素数が異なる配列
type Different = Concat<[1], [2, 3, 4]>; // [1, 2, 3, 4]

すべてのケースで、型が正確に推論されます。

まとめ

この記事では、type-challenges の Concat 問題を通じて、以下の TypeScript の重要な概念を学びました:

  • ジェネリック型パラメータ: 再利用可能な型定義の作成
  • 型制約 (extends readonly unknown[]): 配列型であることを保証し、readonly 配列も受け入れる柔軟な型安全性を確保
  • バリアディックタプル型: タプル型の要素を柔軟に展開・結合する機能
  • スプレッド構文 (...T): 型レベルでの配列要素の展開
  • タプル型の型保持: 各要素の型情報を完全に保持したまま結合できる

この実装により、JavaScript の Array.concat() の動作を型レベルで正確に再現できることがわかりました。

参考リンク

Discussion