はじめに
実務でTypeScriptを使っていてよく見かける、(typeof ConstObject)[keyof typeof ConstObject] のパターンについて、この型定義が何を意味し、どのように機能するのかをコード例を交えながら解説します。
一見すると複雑に見えますが、これは 定数オブジェクトの「値」を型として安全に利用 するための非常に便利なテクニックです。
目次
🧷 はじめに: なぜこの型定義が必要なのか?
JavaScriptやTypeScriptで、特定の文字列のセットだけを受け入れたい場合、どうするでしょうか?
例えば、クラスメイトが「MIYANO」と「KAYANO」の2種類しかない場合を考えてみましょう。
// 悪い例: マジックストリング
function processClassmate(name: string) {
if (name === 'MIYANO') {
// MIYANOの処理
} else if (name === 'KAYANO') {
// KAYANOの処理
} else {
// エラー
}
}
processClassmate('MIYANO');
processClassmate('KAYANO');
processClassmate('InvalidName’); // コンパイルエラーにならない!
この「マジックストリング」の問題点は、'InvalidName'のようなタイプミスがあってもTypeScriptが検知してくれないことです。
これを解決するために、TypeScriptの強力な型システムを活用します。
🧷 型定義の分解とステップバイステップ解説
では、classmateName: (typeof ClassmateName)[keyof typeof ClassmateName]という型定義を分解して見ていきましょう。
まず、前提となるClassmateNameという定数オブジェクトがあります。
export const ClassmateName = {
Miyano: 'MIYANO',
Kayano: 'KAYANO',
} as const;
ここで重要なのは、as constという記述です。
- これは「constアサーション」と呼ばれ、オブジェクトのプロパティをすべて
readonlyにし、その値をリテラル型として扱わせるためのものです。 - これにより、TypeScriptは
'MIYANO'や'KAYANO'といった具体的な文字列を型として認識できるようになります。
typeof ClassmateName
これは、ClassmateNameという変数そのものの型を取得します。
上記のClassmateNameの場合、typeof ClassmateNameは次のような型になります。
{
readonly Miyano: "MIYANO";
readonly Kayano: "KAYANO";
}
これは、ClassmateNameオブジェクトが持つプロパティとその値の型を正確に表現しています。
keyof typeof ClassmateName
次に、keyof演算子が登場します。
-
keyofは、与えられた型のすべてのプロパティ名(キー)を文字列リテラルのユニオン型として抽出されます。 -
typeof ClassmateNameが{ readonly Miyano: "MIYANO"; readonly Kayano: "KAYANO"; }なので、keyof typeof ClassmateNameは次のような型になります。
"Miyano" | "Kayano"
これは、ClassmateNameオブジェクトのキーである"Miyano"と"Kayano"のどちらかであることを示す型です。
(typeof ClassmateName)[keyof typeof ClassmateName]
最後に、これら二つを組み合わせます。
- これは 「インデックスアクセス型」 と呼ばれるもので、オブジェクトの型に対して、そのキーの型を指定することで、対応する値の型を取得します。
-
typeof ClassmateNameがオブジェクトの型、keyof typeof ClassmateNameがキーのユニオン型なので、結果としてClassmateNameオブジェクトの値のユニオン型が抽出されます。
"MIYANO" | "KAYANO"
つまり、classmateNameというプロパティは、'MIYANO'または'KAYANO'のいずれかの文字列リテラル値しか受け付けない、という型定義になります。
🧷 具体的なコード例で理解を深める
以下ValidatedClassmateという型が定義されており、その中でclassmateNameプロパティにこの型が適用されています。
export type ValidatedClassmate = {
key: string;
roomId: number;
classmateName: 'MIYANO' | 'KAYANO'; // ここで型が適用されている
};
これにより、ValidatedClassmate型のオブジェクトを作成する際に、
classmateNameには'MIYANO'か'KAYANO'のどちらかしか指定できなくなり、
それ以外の値を指定するとコンパイルエラーが発生します。
const validClassmate: ValidatedClassmate = {
key: '1',
roomId: 101,
classmateName: 'MIYANO', // OK
};
const invalidClassmate: ValidatedClassmate = {
key: '2',
roomId: 102,
classmateName: 'InvalidType', // Type '"InvalidType"' is not assignable to type '"MIYANO" | "KAYANO"'.
};
以下では、複数の定数オブジェクトに対して同様のパターンで型を生成し、利用しています。
export const AttendStatus = {
Attendance: 'ATTENDANCE',
Absence: 'ABSENCE',
} as const;
export type AttendStatusType = (typeof AttendStatus)[keyof typeof AttendStatus]; // 'ATTENDANCE' | 'ABSENCE'
🧷 このパターンのメリット
この型定義パターンを使用する主なメリットは以下の通りです。
-
型安全性の向上
特定の文字列リテラルのみを許可することで、タイプミスや不正な値の代入によるランタイムエラーを防ぎます。 -
コードの可読性と保守性の向上
利用可能な値が定数オブジェクトとして一箇所にまとめられているため、コードを読む人がどのような値が許容されるのかを簡単に理解できます。
また、新しい値を追加する際も、定数オブジェクトを更新するだけで関連するすべての型定義が自動的に更新されます。 -
マジックストリングの排除
コード中に直接文字列をハードコーディングする「マジックストリング」を排除し、より堅牢なコードベースを構築できます。
🧷 まとめ
-
classmateName: (typeof ClassmateName)[keyof typeof ClassmateName]という型定義は、TypeScriptのtypeof、keyof、そしてインデックスアクセス型を組み合わせた強力なテクニック - これにより、定数オブジェクトの「値」を型として抽出し、コードの型安全性、可読性、保守性を大幅に向上させるだけでなく、より堅牢でメンテナンスしやすいアプリケーションを開発に繋がる。