🔄

【type-challengesに挑戦!】Tuple to Object の解答と解説

に公開

はじめに

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

問題

配列が与えられた時、それをオブジェクト型に変換します。キーと値の両方が、与えられた配列の要素でなければなりません。

例:

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> 
// 期待される結果: { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

解答

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P;
};

解説

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

type TupleToObject<T extends readonly (string | number | symbol)[]>

型パラメータ T に制約を設けています:

  • readonly (string | number | symbol)[]: T は読み取り専用の配列型でなければならない
  • 配列の要素は string | number | symbol のいずれかの型である必要がある
    • これらはオブジェクトのキーとして使用できる型です(PropertyKey 型)

なぜこの制約が必要か

// ✅ OK: as const で readonly な配列型になる
const tuple = ['tesla', 'model 3', 'model X'] as const;
type Result = TupleToObject<typeof tuple>;

// ✅ OK: number も使用可能
const numbers = [1, 2, 3] as const;
type NumberResult = TupleToObject<typeof numbers>;

// ❌ エラー: オブジェクトはキーとして使用できない
const invalid = [{}, {}] as const;
type InvalidResult = TupleToObject<typeof invalid>;
// Type '{}' does not satisfy the constraint 'string | number | symbol'.

ステップ 2: インデックスアクセス型 T[number]

T[number]

この構文は配列型のすべての要素の型をユニオン型として取得します:

具体例

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const;
type Tuple = typeof tuple;
// 型: readonly ['tesla', 'model 3', 'model X', 'model Y']

type Elements = Tuple[number];
// 型: 'tesla' | 'model 3' | 'model X' | 'model Y'

なぜ [number] を使うのか

配列のインデックスは数値なので、T[number] は「配列 T のすべての数値インデックスでアクセスできる要素の型」を意味します。

type Array1 = ['a', 'b', 'c'];
type Element1 = Array1[number]; // 'a' | 'b' | 'c'

type Array2 = [string, number, boolean];
type Element2 = Array2[number]; // string | number | boolean

// 特定のインデックスを指定することもできる
type Array3 = ['first', 'second', 'third'];
type First = Array3[0];   // 'first'
type Second = Array3[1];  // 'second'
type Third = Array3[2];   // 'third'

ステップ 3: マップド型 [P in T[number]]: P

{
  [P in T[number]]: P;
}

この構文はマップド型で、以下の処理を行います:

  1. [P in T[number]]: T[number] のユニオン型から各要素を P として反復処理
  2. : P: プロパティの値の型も P と同じにする

具体例で見てみましょう

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const;
type Result = TupleToObject<typeof tuple>;

展開プロセス:

  1. Treadonly ['tesla', 'model 3', 'model X', 'model Y']
  2. T[number]'tesla' | 'model 3' | 'model X' | 'model Y'
  3. P in T[number]P が順に各文字列リテラル型になる
  4. P について、[P]: P のプロパティを作成
  5. 結果:
{
  'tesla': 'tesla';
  'model 3': 'model 3';
  'model X': 'model X';
  'model Y': 'model Y';
}

完全な動作例

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P;
};

// 文字列の配列
const carModels = ['tesla', 'model 3', 'model X', 'model Y'] as const;
type CarModels = TupleToObject<typeof carModels>;
// 型: { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y' }

// 数値の配列
const statusCodes = [200, 404, 500] as const;
type StatusCodes = TupleToObject<typeof statusCodes>;
// 型: { 200: 200, 404: 404, 500: 500 }

// シンボルの配列
const sym1 = Symbol('a');
const sym2 = Symbol('b');
const symbols = [sym1, sym2] as const;
type Symbols = TupleToObject<typeof symbols>;

// 実際に使用する例
const carObj: CarModels = {
  'tesla': 'tesla',
  'model 3': 'model 3',
  'model X': 'model X',
  'model Y': 'model Y'
};

// ✅ OK: すべてのプロパティが必要
const validObj: CarModels = {
  'tesla': 'tesla',
  'model 3': 'model 3',
  'model X': 'model X',
  'model Y': 'model Y'
};

// ❌ エラー: プロパティが不足
const invalidObj1: CarModels = {
  'tesla': 'tesla',
  'model 3': 'model 3'
};
// Property 'model X' is missing

// ❌ エラー: 値が正しくない
const invalidObj2: CarModels = {
  'tesla': 'wrong',  // 型エラー
  'model 3': 'model 3',
  'model X': 'model X',
  'model Y': 'model Y'
};
// Type '"wrong"' is not assignable to type '"tesla"'

なぜキーと値が同じ型になるのか

マップド型の構文 [P in T[number]]: P において:

  • キー部分 ([P in T[number]]): P はプロパティ名になる
  • 値部分 (: P): 同じ P が値の型になる

これにより、キーと値が必ず同じリテラル型になります:

// P が 'tesla' の場合
// { 'tesla': 'tesla' } というプロパティが作成される

// P が 'model 3' の場合
// { 'model 3': 'model 3' } というプロパティが作成される

まとめ

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

  1. 型制約 (extends): 型パラメータに制限を設けて型安全性を確保
  2. インデックスアクセス型 (T[number]): 配列のすべての要素の型をユニオン型として取得
  3. マップド型: ユニオン型から各要素を反復してオブジェクト型を構築
  4. PropertyKey 型: オブジェクトのキーとして使用できる型(string | number | symbol

この型ユーティリティは、配列の要素からオブジェクト型を生成する際に、キーと値の型安全性を保証する強力なパターンです。

参考リンク

Discussion