こんにちは!TypeScriptの型システムの世界へようこそ。フルスタックアーキテクトの私です。
今回は、実務の現場で「おっ、これ分かってるね!」と周りから一目置かれるようになる、非常にスマートなテクニックについてお話ししますね。
皆さんは、関数を作る時に「特定の文字列しか受け付けないようにしたい」と思ったことはありませんか? 例えば、APIのエンドポイントや、ユーザーの権限(ロール)などを制限したい時です。
そんな時、ナイーブ(素朴)なやり方でコードを書いていると、後から仕様変更があった時に地獄のような修正地獄が待っています。今回は、`as const` と `typeof` というTypeScriptの強力な武器を組み合わせて、そのメンテナンスコストをゼロにする極意を伝授します。
ここをクリアすれば、TypeScriptの型システムの「本質」がグッと見えてきますよ。一緒にバッチリマスターしていきましょう!
—
1. 痛みを伴う「よくある実装」と、その限界
まずは、私たちがやりがちな「ちょっと残念なコード」を見てみましょう。ユーザーの権限(ロール)を受け取る関数を想定してください。
// 権限を表す文字列だけを受け取る関数
function checkUserAccess(role: “admin” | “editor” | “viewer”) {
console.log(`現在の権限: ${role}`);
}
// 使う時はこうですよね
checkUserAccess(“admin”); // OK
// checkUserAccess(“super-user”); // ちゃんとコンパイルエラーになる!
一見、これでも動きますし、型チェックも効いています。しかし、「仕様変更」が起きた瞬間にボロが出ます。
もし、将来的に「`moderator`(モデレータ)」という新しい権限が追加されたらどうなるでしょうか?
この関数を使っているあらゆるコンポーネント、テストコード、別のファイルにある型定義など、文字列 “admin” | “editor” | … がハードコーディングされている箇所をすべて探し出して書き換える必要があります。
これでは、プロジェクトが大きくなるにつれてメンテナンスコストが爆発してしまいますよね。
—
2. 救世主:`as const` と `typeof` による動的型生成
この問題を鮮やかに解決するのが、「定数定義から型を自動生成する」というアプローチです。
まずは実際のコードを見てみてください。
// ① 外部定数として、許可する値をオブジェクト(または配列)で定義する
const USER_ROLES = {
ADMIN: “admin”,
EDITOR: “editor”,
VIEWER: “viewer”,
} as const; // ← ここが魔法のキーワード!
// ② 定数から「型」を自動で抽出し、エイリアス(別名)を作る
type UserRole = typeof USER_ROLES[keyof typeof USER_ROLES];
// ③ 関数には、その自動生成された型を引数に指定する
function checkUserAccess(role: UserRole) {
console.log(`現在の権限: ${role}`);
}
// 使い方
checkUserAccess(USER_ROLES.ADMIN); // “admin” が渡されるのでOK!
なんだか見慣れない記号が出てきましたね。でも安心してください。頭の中でコンパイラの動きをトレースすれば、すごく理にかなった仕組みだということが分かります。
魔法の正体を分解して解説します
1. `as const` (TypeScriptの最強の型推論:const assertion)
普通、TypeScriptのオブジェクトは「あとからプロパティが書き換わるかもしれない」と予測して、値の型を一般的な `string` や `number` として広めに解釈します。
しかし、末尾に `as const` をつけると、TypeScriptに対して「このオブジェクトは絶対に書き換えられない(イミュータブルな)固定値だから、値そのものを型として記憶して!」と命令できます。
これにより、`USER_ROLES.ADMIN` は単なる `string` ではなく、文字通り `”admin”` という型(リテラル型)になります。
2. `typeof USER_ROLES`
これは「JavaScriptの値(オブジェクト)」から「TypeScriptの型」を逆算して取り出す演算子です。この時点での型は以下のようになっています。
{
readonly ADMIN: “admin”;
readonly EDITOR: “editor”;
readonly VIEWER: “viewer”;
}
3. `[keyof typeof USER_ROLES]`(インデックスアクセス型)
オブジェクトの「キーの型(`”ADMIN” | “EDITOR” | “VIEWER”`)」を使って、オブジェクトが持つ「値の型」を一網打尽にすくい上げるテクニックです。
結果として、`”admin” | “editor” | “viewer”` というユニオン型(合体の型)が綺麗に完成します。
—
3. この手法がもたらす圧倒的なメリット
この書き方にしておくと、先ほどお話しした「仕様変更」が起きた時どうなるでしょうか?
もし「`moderator`」を追加したくなったら、定数の定義を1箇所書き換えるだけで済みます。
const USER_ROLES = {
ADMIN: “admin”,
EDITOR: “editor”,
VIEWER: “viewer”,
MODERATOR: “moderator”, // ここに足すだけ!
} as const;
// type UserRole も、関数の引数も、自動的に “admin” | “editor” | “viewer” | “moderator” に進化します!
関数側や他のファイルを書き換える必要は一切ありません。これが、「単一責任の原則(DRY原則)」を型レベルまで落とし込んだ、プロフェッショナルな設計です。
—
4. 初学者が陥りがちな文法エラーと注意点
ここで、よくあるハマりポイントを先回りして共有しておきますね。
❌ やってしまいがちなミス:`as const` を忘れる
const USER_ROLES = {
ADMIN: “admin”,
EDITOR: “editor”,
}; // as const がない!
type UserRole = typeof USER_ROLES[keyof typeof USER_ROLES];
// この場合、UserRole は “admin” | “editor” ではなく、ただの `string` になってしまいます。
理由: `as const` がないと、TypeScriptは「あとから `USER_ROLES.ADMIN = “super-admin”` と書き換えられるかもしれない」と優しく(大きなお世話で)忖度し、型を `string` に広げてしまうからです。型が `string` になってしまうと、どんな文字列でも関数に渡せてしまい、リテラル型の意味がなくなってしまいます。
—
まとめ
いかがでしたでしょうか?
- `as const` で、オブジェクトや配列を「変更不可能なリテラル」としてTypeScriptに教え込む。
- `typeof` とインデックスアクセスを組み合わせて、その実体からスマートに型を抽出する。
この2ステップをマスターするだけで、あなたの書くTypeScriptコードは一気に洗練され、変更に強い堅牢なものに生まれ変わります。
「定数を変えたら、型も勝手に追従してくれる」。この心地よさを知ってしまったら、もう元のハードコーディングには戻れなくなりますよ。
ぜひ、あなたの開発現場でも試してみてくださいね。応援しています!