🎯
【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[]:TとUが配列型(タプル型)であることを保証し、通常の配列と 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]>;
この場合、以下のように展開されます:
-
Tは[1] -
Uは[2] -
[...T, ...U]は[...[1], ...[2]] - 結果:
[1, 2]
より複雑な例:
type Example2 = Concat<[1, 2], [3, 4]>;
展開プロセス:
-
Tは[1, 2] -
Uは[3, 4] -
[...T, ...U]は[...[1, 2], ...[3, 4]] - 結果:
[1, 2, 3, 4]
異なる型が混在する場合:
type Example3 = Concat<['1', 2, '3'], [false, boolean, '4']>;
展開プロセス:
-
Tは['1', 2, '3'] -
Uは[false, boolean, '4'] -
[...T, ...U]は[...['1', 2, '3'], ...[false, boolean, '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() の動作を型レベルで正確に再現できることがわかりました。
参考リンク
- https://github.com/type-challenges/type-challenges
- https://github.com/type-challenges/type-challenges/blob/main/questions/00533-easy-concat/README.md
- https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types
- https://www.typescriptlang.org/docs/handbook/2/generics.html
- https://devblogs.microsoft.com/typescript/announcing-typescript-4-0/#variadic-tuple-types
Discussion