【実務・中級編】配列の要素型を動的に取得する:Indexed Access Typesの活用 – TypeScript コア・型システムの基礎解析バイブル

開発現場のコードレビューをしていると、いまだにこんな記述に出くわすことがある。

// よくある「修正漏れ爆弾」を抱えたコード
const USER_ROLES = [‘admin’, ‘editor’, ‘viewer’] as const;

// 別のファイルや型定義でわざわざ手動で型を再定義している
type UserRole = ‘admin’ | ‘editor’ | ‘viewer’;

おいおい、待ってくれ。配列の定義と型の定義を2箇所で行っている時点で、DRY原則(Don’t Repeat Yourself)に反している。将来、要件定義の変更で `’moderator’` というロールが追加されたとき、果たしてこの2つのファイルを完璧に同時に修正し続けられるだろうか? 答えは「No」だ。人間の記憶力や注意力に依存した設計は、遅かれ早かれプロダクション環境で型矛盾という名のバグを引き起こす。

TypeScriptの型システムは、ただの「静的チェッカー」ではない。値の空間(Value space)から型の空間(Type space)へと、動的に情報を抽出・再構築するための強力なメタプログラミングエンジンなのだ。

今回は、配列の定義からその要素型をミリ単位の狂いもなく自動抽出し、データ構造の変更に完全追従する堅牢なアーキテクチャを築くための「Indexed Access Types(インデックスアクセス型)」の極意を伝授しよう。

—

1. なぜ `as const` と Indexed Access Types が最強のペアなのか?

まず、TypeScriptのコンパイラが型をどう評価しているかを押さえておこう。
通常の配列リテラルは、広範な型(Widening)推論が働き、単なる `string[]` として評価される。これでは要素の特定など不可能だ。

ここで `as const`(Const Assertion)の出番となる。これを使うことで、配列は「変更不可能なタプル(Readonly Tuple)」へと昇格し、各要素はリテラル型として完全に固定される。

const PERMISSIONS = [‘read’, ‘write’, ‘delete’] as const;
// 評価される型: readonly [“read”, “write”, “delete”]

このタプル型に対して Indexed Access Types (`[number]`) を適用する。

type Permission = typeof PERMISSIONS[number];
// 評価される型: “read” | “write” | “delete”

ここでやっていることは何か?
JavaScriptの配列からインデックスで要素を取り出す構文(`array[0]` や `array[i]`)と全く同じメンタルモデルで、型空間における配列型(またはタプル型)から、数値型(`number`)をキーとしてすべての要素の型を一括して引き抜いているのだ。

配列の定義がどう変わろうとも、この `[number]` アクセスを通じた型はコンパイル時に自動再計算される。型定義の「二重管理」は、この瞬間から完全に消滅する。

—

2. 【実践】API連携・フロントエンド設計におけるプロダクションコード例

実務で最も恩恵を受けるシーンとして、「APIから返却されるステータス定義」と「それを利用するコンポーネントのProps設計」を考えてみよう。

以下のコードは、単に動くだけでなく、コンパイラの型推論を極限まで活かした保守性の高いモダンな実装だ。

/

  • 注文ステータスのマスター定義
  • 変更時はここをいじるだけで、依存するすべてのUI・ロジックの型が追従する

/
export const ORDER_STATUSUS = {
PENDING: ‘PENDING’,
PAID: ‘PAID’,
SHIPPED: ‘SHIPPED’,
CANCELLED: ‘CANCELLED’,
} as const;

// オブジェクトの値の型を配列として抽出
const STATUS_ARRAY = Object.values(ORDER_STATUSUS) as readonly (typeof ORDER_STATUSUS)[keyof typeof ORDER_STATUSUS][];

// もし純粋な配列から始めるなら以下でも同等
const STATUS_LIST = [‘PENDING’, ‘PAID’, ‘SHIPPED’, ‘CANCELLED’] as const;

// 【極意】配列からユニオン型を導出
export type OrderStatus = typeof STATUS_LIST[number];
// 成果物: “PENDING” | “PAID” | “SHIPPED” | “CANCELLED”

/

  • 実務ユースケース1: 状態に応じたラベルマッピングの強制
  • 網羅性チェック(Exhaustiveness check)をコンパイル時に担保する

/
export const ORDER_STATUS_LABELS: Record = {
PENDING: ‘処理待ち’,
PAID: ‘支払い完了’,
SHIPPED: ‘発送済み’,
CANCELLED: ‘キャンセル’,
// 万が一、OrderStatusに新しい値が追加され、ここに書き忘れると
// TS2322: Type ‘…’ is not assignable to type ‘Record‘ のコンパイルエラーが出る
};

/

  • 実務ユースケース2: ドリブンなコンポーネントProps設計

/
import React from ‘react’;

interface StatusBadgeProps {
// 文字列型ではなく、厳格に抽出されたOrderStatusのみを受け入れる
status: OrderStatus;
}

export const StatusBadge: React.FC = ({ status }) => {
// 実行時の安全性を担保しつつ、ビューを描画
return (

{ORDER_STATUS_LABELS[status]}

);
};

—

3. コンパイラ性能と実務上の注意点(アンチパターン)

ここで、アーキテクトとしてパフォーマンスと型推論の罠についても言及しておかなければならない。

❌ 避けるべきアンチパターン:複雑すぎるネストと動的関数からの型抽出

大規模なコードベースにおいて、深すぎるオブジェクトのネストや、関数の実行結果の型(`ReturnType`)を経由して無理やり配列の要素型を取ろうとすると、TypeScriptの型チェッカー(TSServer)に甚大な負荷がかかる。

// 悪い例:型推論の迷路を生み出す
const complexData = generateComplexData(); // 戻り値が巨大な型ツリーを持つ関数
type BadType = typeof complexData.items[number][‘details’][‘history’][number];

このような記述が数十ファイルに散らばると、IDEの補完速度(LSPの応答速度)が著しく低下し、開発体験(DX)が崩壊する。

✅ 正しいアプローチ:データ構造の「単一の真実の源(Single Source of Truth)」を明確にする

基本は「プリミティブな定数配列(`as const`)」または「小さく分割されたスキーマ定義」からIndexed Access Typesを引くこと。これならばコンパイラの評価コストは最小限であり、一瞬で型が解決される。

—

4. まとめ:型は「書くもの」ではなく「導出するもの」

優れたTypeScriptエンジニアと、そうでないエンジニアの決定的な違いは何か?
それは、「型を手で書いていないか」だ。

`string` や `number`、あるいはハードコードされたユニオン型(`type Role = ‘admin’ | ‘user’`)を自分の手でタイピングしているうちは、まだTypeScriptの真のパワーを引き出せていない。

  • 値を定義したら、`as const` で固める。
  • そこから `typeof` と `[number]`(あるいは `[keyof typeof …]`)で型を導出(Derive)する。

この鉄則をチームの共通認識として徹底してほしい。データ構造の変更にびくともしない、美しく堅牢なコードベースを手に入れられるはずだ。コードレビューで手動の型定義を見かけたら、迷わずこのIndexed Access Typesをサジェストしてあげてほしい。

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