コードレビューをしていたら、次のようなコードに出くわしたことはないだろうか。
// よく見るが、メンテナンス性の低いアンチパターン
function setAlignment(align: ‘left’ | ‘right’ | ‘center’) {
// …
}
const ALIGN_CONFIG = {
left: ‘left’,
right: ‘right’,
center: ‘center’,
} as const;
一見して何が問題かわかるだろうか?
UIのレイアウト定義やAPIのエンドポイント、あるいはデザインシステムのトークンなど、「定数として定義した値の集合」と「関数が受け入れるべき型の集合」が二重管理されている点だ。仕様変更で `’justify’` が追加された瞬間、開発者はオブジェクトの定義と関数の引数型の両方を書き換えなければならない。これは「DRY原則(Don’t Repeat Yourself)」の重大な違反であり、必ずヒューマンエラーを生む温床となる。
TypeScriptの型システムは、ランタイムの値を型へと昇格させる強力なメカニズムを持っている。今回は、`as const` と `typeof` を極限まで活用し、「外部定数から引数の許容値を動的に生成し、型安全とメンテナンス性を完全両立させる設計パターン」を、コンパイラの挙動の裏側まで含めて伝授する。
—
1. なぜ `as const` と `typeof` なのか?(型評価のメカニズム)
TypeScript初心者によくある誤解が、「JavaScriptのオブジェクトをそのまま型として使おうとする」ことだ。
通常のオブジェクト定義は、コンパイル時にプリミティブ型(`string` など)へと広げられる(Widening)。
しかし、オブジェクトの末尾に `as const`(Const Assertion)を付与すると、TypeScriptコンパイラはそのオブジェクトを「読み取り専用(readonly)であり、かつ、とりうる値がリテラル型そのものである構造体」として評価する。
さらに、`typeof` 演算子は、ランタイムの値を「型空間」に持ち上げる。この2つが組み合わさった瞬間、「単なるデータ構造」が「唯一無二の型定義ソース」へと変貌するのだ。
—
2. 実践:プロダクションコードにおける堅牢な設計パターン
では、実際のフロントエンド開発やコンポーネント設計を想定した、洗練されたコードを見ていこう。
テーマは、デザインシステムの「ブレイクポイント」と「バリアント」を動的に管理するUI関実装だ。
/
- ==========================================
- 1. 唯一の真実のソース(Single Source of Truth)
- ==========================================
- デザインシステムの定義をここに集約する。
- as constにより、プロパティはすべて readonly のリテラル型になる。
/
export const UI_CONFIG = {
breakpoints: {
mobile: ‘320px’,
tablet: ‘768px’,
desktop: ‘1024px’,
wide: ‘1440px’,
},
variants: {
primary: ‘bg-blue-600 text-white’,
secondary: ‘bg-gray-200 text-gray-800’,
danger: ‘bg-red-600 text-white’,
},
} as const;
/
- ==========================================
- 2. 型の導出(Derived Types)
- ==========================================
- typeof と keyof,Indexed Access Types を組み合わせ、
- 定数オブジェクトから自動的に型を抽出し、再エクスポートする。
/
// ブレイクポイントの「キー」の型: ‘mobile’ | ‘tablet’ | ‘desktop’ | ‘wide’
export type BreakpointKey = keyof typeof UI_CONFIG.breakpoints;
// ブレイクポイントの「値」の型: ‘320px’ | ‘768px’ | …
export type BreakpointValue = typeof UI_CONFIG.breakpoints[BreakpointKey];
// バリアントの型: ‘primary’ | ‘secondary’ | ‘danger’
export type ButtonVariant = keyof typeof UI_CONFIG.variants;
/
- ==========================================
- 3. 導出された型を利用する関数・コンポーネント設計
- ==========================================
/
interface ApplyStylesParams {
// 外部定数から完全同期された型を引数に指定
variant: ButtonVariant;
minWidthBreakpoint?: BreakpointKey;
}
/
- スタイルを適用するコアロジック関数
- @param params 設定パラメータ
- @returns 適用されるCSSクラス文字列
/
export function resolveComponentStyles(params: ApplyStylesParams): string {
const baseStyle = UI_CONFIG.variants[params.variant];
// オプショナル引数のハンドリングも完全に型安全
if (params.minWidthBreakpoint) {
const minWidth = UI_CONFIG.breakpoints[params.minWidthBreakpoint];
// 実行時には具体的な文字列として安全に処理される
console.log(`Applying media query for min-width: ${minWidth}`);
}
return baseStyle;
}
// — 【使用例】 —
// 1. 正常系:定義済みのキーを渡す(IDEの補完も完璧に効く)
const style1 = resolveComponentStyles({
variant: ‘primary’,
minWidthBreakpoint: ‘desktop’,
});
// 評価結果: ‘bg-blue-600 text-white’
// 2. 異常系:タイポや未定義の文字列を渡すと、コンパイルエラーになる
const style2 = resolveComponentStyles({
variant: ‘primry’, // Error: Type ‘”primry”‘ is not assignable to type ‘ButtonVariant’. Did you mean ‘primary’?
});
—
3. この設計がもたらす圧倒的なメリット
コードレビューで後輩や同僚にこのパターンを推奨する際、私は決まって次の3つの優位性を説いている。
1. 認知的負荷のゼロ化(Single Source of Truth)
「この関数の引数には何が渡せるんだっけ?」と、実装ファイルと型定義ファイルを往復する必要は二度とない。コードを読むときは `UI_CONFIG` を見ればすべてが完結する。
2. リファクタリング耐性の向上
将来的にデザインシステムが拡張され、`UI_CONFIG.variants` に `’success’` が追加された瞬間、何の手動変更もなしに `ButtonVariant` 型は自動更新され、それを引数に取る全関数が正しく新しい値を受け入れられるようになる。
3. ランタイムと静的解析の完全な調和
JavaScriptのオブジェクトとしてそのままコンポーネントのレンダリングや設定値の参照(`UI_CONFIG.variants.primary`)に使えるため、DRY原則を保ちつつ「コードの二重化」を完全に排除できる。
—
4. チーフアーキテクトからの実践的アドバイス:パフォーマンスとコンパイル速度への配慮
最後に、大規模なプロダクトでこの手法を使う際の「現場の知見」を共有しておこう。
`as const` を巨大な設定オブジェクト(例えば数千行に及ぶAPIのスキーマ定義や翻訳辞書など)に付与すると、TypeScriptコンパイラ(tsserver)がすべてのプロパティを深くまでリテラル型として推論・キャッシュするため、IDEの型チェック速度(ホバー時の応答性など)にわずかながら負荷がかかることがある。
もしプロジェクトの規模が巨大化し、型の評価コストがボトルネックになった場合は、以下のように「必要な部分だけ」を切り出して `as const` を適用するか、明確に型アサーションの境界線を引くこと。
// 巨大なオブジェクトの一部だけを型抽出のソースにする
const HUGE_API_DICTIONARY = {
// … 膨大な設定
statuses: {
ACTIVE: ‘ACTIVE’,
PENDING: ‘PENDING’,
DELETED: ‘DELETED’,
}
} as const; // 必要な階層に絞る、または個別に分割する
export type ApiStatus = keyof typeof HUGE_API_DICTIONARY.statuses;
まとめ
型定義を手打ちで二重管理する時代は終わった。
「データが型を支配する(Data-Driven Types)」というパラダイムシフトを受け入れ、`as const` と `typeof` を使いこなすことで、君のコードベースはより強固で、美しく、拡張性にあふれたアーキテクチャへと昇華するだろう。
次のプルリクエストから、不要な手動のunion型(`type X = ‘a’ | ‘b’`)を削除し、定数からの動的生成に置き換えてみせるといい。コードレビューの景色がガラリと変わるはずだ。