【実務・中級編】配列の要素をユニオン型に変換する:keyofとtypeofの合わせ技 – TypeScript コア・型システムの基礎解析バイブル

コードレビューで見落とされがちな「型の二重管理」という悪夢

フロントエンドの実装において、次のようなコードに直面したことはないだろうか。

// 1. 定数配列を定義
const SUPPORTED_LOCALES = [‘ja’, ‘en’, ‘es’, ‘fr’] as const;

// 2. 謎の「手動ユニオン型」をわざわざ別途定義する
type Locale = ‘ja’ | ‘en’ | ‘es’ | ‘fr’;

このコードを見た瞬間、私はレビューコメントでこう指摘する。「なぜ、真実のソース(Source of Truth)が2つ存在しているのか?」と。

定数配列に要素を追加・削除するたびに、手動でユニオン型を書き換える。この手法は、開発者が忘れた瞬間にランタイムと型定義の乖離を生み出し、型安全神話を内側から崩壊させる最も安易なアンチパターンだ。

今回は、TypeScriptのコンパイラが持つ型評価のメカニズムを極限まで利用し、配列から完璧なユニオン型を動的に導出する `keyof typeof` の合わせ技と、それをプロダクションコードでどう昇華させるべきか、チーフアーキテクトの視点から徹底的に解説しよう。

—

メカニズムの解剖:なぜ `keyof typeof` なのか?

まず、TypeScriptコンパイラがこのコードをどう解釈しているのか、その深層を覗く。

const ROLES = [‘admin’, ‘editor’, ‘viewer’] as const;

ここで `as const`(const assertion)をつけることがすべての出発点だ。これがないと、型は単なる `string[]` に widening(型の拡大)され、配列の中身の文字列リテラルは永遠に失われる。`as const` を付与することで、型は以下のような「読み取り専用のタプル型」へと固定される。

readonly [“admin”, “editor”, “viewer”]

ここに `typeof` 演算子を適用すると、この値の型が抽出される。
さらに、JavaScript/TypeScriptにおいて、配列(タプル)は「数値のインデックスをキーに持つオブジェクト」に他ならない。そのため、この型に対して `keyof` を適用するとどうなるか。

type RoleKeys = keyof typeof ROLES;
// 評価結果: 0 | 1 | 2 | “length” | “readonly” | … (配列のプロパティ名やインデックス)

私たちが欲しいのはインデックスではなく、配列の要素そのものだ。ここでTypeScriptのイン덱アクセス型(Indexed Access Types)の登場となる。配列のすべての要素型を引くために、インデックスとして数値型全般(number)を指定する。

type Role = (typeof ROLES)[number];
// 評価結果: “admin” | “editor” | “viewer”

これが、配列からユニオン型を完璧に生成するメタプログラミングの基本イディオムである。`keyof` すら不要なケースが多いが、キーと値の双方向マッピングが必要な複雑な設計においては、この二つを組み合わせることでコンパイラを完全に手懐けることができる。

—

プロダクションコード:API連携とコンポーネント設計への応用

では、これを実際のフロントエンド開発(APIバリデーション、多言語対応、UIコンポーネントのバリアント管理など)にどう応用するか。コピペで即座に使える堅牢な実例を示そう。

/

  • 1. 真実のソース(Source of Truth)としての定数配列

/
export const HTTP_METHODS = [‘GET’, ‘POST’, ‘PUT’, ‘DELETE’, ‘PATCH’] as const;

/

  • 2. 配列からユニオン型を動的生成
  • (手動での二重管理は一切排除する)

/
export type HttpMethod = (typeof HTTP_METHODS)[number];

/

  • 3. 実用例:実行時バリデーション関数への応用
  • 渡された文字列が本当に HttpMethod かどうかを、型ガード(Type Guard)として安全に判定する

/
export function isHttpMethod(value: unknown): value is HttpMethod {
// 実行時チェック:HTTP_METHODS 配列の中に含まれているか
return HTTP_METHODS.includes(value as HttpMethod);
}

// — コンポーネント設計での実践 —

import React from ‘react’;

interface ButtonProps {
// コンポーネントのバリアントも、ベースとなる定数配列から導出する
variant: (typeof BUTTON_VARIANTS)[number];
children: React.ReactNode;
}

const BUTTON_VARIANTS = [‘primary’, ‘secondary’, ‘danger’, ‘ghost’] as const;
export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];

export const DynamicButton: React.FC = ({ variant, children }) => {
// コンパイル時に variant は ‘primary’ | ‘secondary’ | ‘danger’ | ‘ghost’ に完全制限される
return (

);
};

—

パフォーマンスとコンパイラ負荷に関する注意点

チーフアーキテクトとして、コンパイラ性能についても言及しておかねばならない。

大規模なコードベースにおいて、このようなユーティリティ型や `as const` を多用しすぎると、TypeScriptの型チェッカー(tsserver)のメモリ消費量が増加し、エディタのインテリセンス(補完)が重くなる現象(Type instantiation is excessively deep and possibly infinite)を引き起こすことがある。

ベストプラクティス:

  • 動的に生成したユニオン型は、必要に応じてモジュール境界で一度別名(Type Alias)にキャッシュ(代入)すること。
  • 無駄なジェネリクスや複雑な条件付き型(Conditional Types)を挟まず、`(typeof ARRAY)[number]` のプリミティブな形を維持することで、コンパイラの評価コストを最小限に抑えられる。

—

まとめ:型の二重管理をコードベースから駆逐せよ

優れたアーキテクチャとは、「DRY原則(Don’t Repeat Yourself)」がコードだけでなく型システムレベルでも徹底されているもののことだ。

  • 値と型を別々に定義しない。
  • 定数配列に `as const` を添え、`(typeof ARRAY)[number]` で型を抽出する。
  • 実行時のロジックとコンパイル時の型安全性を完全に同期させる。

このパターンをチームに定着させれば、仕様変更で配列の要素を追加した瞬間に、対応漏れのあるスイッチ文やAPIクライアントのコードがビルドエラーとして検知されるようになる。

今日からあなたのプロジェクトにある「手動で書かれたユニオン型」をすべて洗い出し、この極限まで洗練されたイディオムに置き換えてほしい。コードの美しさと堅牢性が劇的に変わるはずだ。

タイトルとURLをコピーしました