こんにちは。フロントエンドチームのシニアアーキテクトだ。
最近、コードレビューをしていて「おっ、これいい感じに型を使いこなしてるな」と感心することもあれば、「おいおい、ここで `any` に逃げたら今までの苦労が水の泡だろ……!」と頭を抱えることも多い。
特に、型定義に `string` 型をそのまま垂れ流しているコードを見かけると、私はそっとコーヒーを飲み干し、こう言いたくなる。
「君のその文字列、本当にただの `string` かい?」と。
TypeScriptの中級から上級への壁、それは「文字列を単なるプリミティブ値ではなく、型パターンの集合として捉えられるか」にかかっている。
今回は、テンプレートリテラル型と `infer` キーワードを組み合わせて、文字列を自在に分解・抽出する「文字列パースの魔術」について、現場の泥臭い知見を交えて徹底的に解説しよう。
—
なぜ、いま「テンプレートリテラル型 × infer」なのか?
実務でフロントエンドを書いていて、次のような絶望感を味わったことはないだろうか?
- URLのパスやルーティングのパラメータ(例: `/users/:id/posts/:postId`)から、動的にパラメータ名だけを型として抽出したい。
- APIから返ってくる特定のプレフィックスを持つイベント名(例: `user:created`, `user:updated`)をパースして、アクション名だけを取り出したい。
- CSSのプロパティ値や、独自のDSL(ドメイン固有言語)を型安全にバリデーションしたい。
昔のTypeScriptであれば、これらはすべて正規表現と、実行時まで分からない `string` 型の海に放り投げられていた。しかし、今のTypeScriptには、コンパイル時に文字列を解体し、型として再構築する強力なエンジンが備わっている。
ブラウザが実行時にJavaScriptを解釈するより遥か手前、V8(TypeScriptコンパイラ)が静的解析を行う裏側で、文字のパターンマッチングと型推論が行われているのだ。この仕組みを使いこなせば、ランタイムエラーの芽をコンパイル時に根こそぎ刈り取ることができる。
—
基礎の確認:テンプレートリテラル型とは
おさらいだ。テンプレートリテラル型は、JavaScriptのテンプレート文字列の構文をそのまま型にしたものだ。
type ActionType = ‘click’ | ‘hover’;
type TargetType = ‘button’ | ‘link’;
// 組み合わせの爆発(ユニオンの直積が自動生成される)
type DOMEvent = `${ActionType}_${TargetType}`;
// 成果物: ‘click_button’ | ‘click_link’ | ‘hover_button’ | ‘hover_link’
これだけでも十分強力だが、真骨頂は「逆方向」、つまり結合された文字列から特定のパーツを引っこ抜くときの発想にある。そこで登場するのが、条件付き型(Conditional Types)の `infer` キーワードだ。
—
実践:`infer` を使った文字列の分解と抽出
文字通り「型推論(infer)」を条件付き型の中で行い、マッチした部分を変数のようにキャプチャするのがこのテクニックのキモだ。
早速、現場で即コピペして使える、実用的なサンプルコードを見ていこう。
/
- プレフィックス付きの識別子から、実際のエンティティ名だけを抽出するユーティリティ型
- 例: “entity:user” -> “user”
/
type ExtractEntity
? EntityName
: never;
// 使用例
type Result1 = ExtractEntity<'entity:user'>; // “user”
type Result2 = ExtractEntity<'entity:article'>; // “article”
type Result3 = ExtractEntity<'not_match_string'>; // never (無視される)
非常にシンプルだが、ここからが本番だ。
もう少し実務で遭遇しそうな、「ルーティングパスからパラメータ名を抽出する型」を作ってみよう。これが書けるようになると、チームメンバーから「おおっ」と一目置かれること間違いなしだ。
応用編:ルーティングパスから `:param` を型として抽出する
例えば、`/api/v1/users/:userId/books/:bookId` というルーティング文字列から、`’userId’ | ‘bookId’` というユニオン型を自動で抽出し、関数の引数の型として強制したいケースを考えてみる。
/
- パス文字列からコロン(:)に続くパラメータ名を再帰的に抽出し、ユニオン型にする
/
type ExtractParams
// パスが ‘/’ で区切られて進んでいくイメージ
T extends `${infer _Start}:${infer Param}/${infer Rest}`
// スラッシュが含まれている場合:現在のパラメータをキャプチャしつつ、残りの文字列(Rest)を再帰的に処理
? Param | ExtractParams
// スラッシュが含まれておらず、末尾にパラメータがある場合
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
// — 動作確認 —
type Path = ‘/api/v1/users/:userId/books/:bookId’;
// 綺麗にパラメータ名だけがユニオン型として抽出される!
type ExtractedParams = ExtractParams
// 結果: type ExtractedParams = “userId” | “bookId”
どうだろう?
文字列の切れ端を `infer` で捕まえ、`Rest`(残りの文字列)を渡して再帰(Recursion)させる。関数型言語のようなアプローチだが、これがTypeScriptの型システムの上で完全に動作する。
—
実務でこのテクニックを活かすベストプラクティス
この強力なパターンマッチングだが、現場で運用する上での注意点(教訓)をいくつか共有しておこう。
1. 複雑怪奇な型にしない(可読性の維持)
再帰や条件付き型を何重にもネストさせると、TypeScriptのコンパイルが重くなるだけでなく、後からコードを引き継いだメンバーが「これ何の呪文書だ?」と絶望する。複雑な文字列操作を行う場合は、型に名前をつけ(Type Alias)、段階的に分解してステップを踏むこと。
2. エラーハンドリング(フォールバック)を忘れない
パターンマッチに失敗したときの挙動を考えておく必要がある。多くの場合、マッチしなかったら `never` を返すか、元の型をそのままスルー(`T` を返す)設計にするのが安全だ。
3. ランタイムのバリデーションとセットで考える
型はあくまでコンパイル時の幻想(セーフティネット)だ。APIレスポンスやユーザー入力など、外部から入ってくる文字列は型だけで安心せず、Zodなどのランタイムバリデーションライブラリと組み合わせるのが、現代の堅牢なフロントエンド開発の鉄則である。
—
おわりに
TypeScriptの型システムは、単なる「エラーを防ぐための型付けの道具」の枠を完全に超え、コンパイル時に走るもう一つのプログラミング言語へと進化している。
今回紹介したテンプレートリテラル型と `infer` を組み合わせた文字列のパースは、一見するとマニアックなテクニックに見えるかもしれない。しかし、共通のルーティングライブラリや、型安全なAPIクライアントを自作する際には、なくてはならない強力な武器になる。
「なんとなく `string` で受けておくか」という妥協を捨て、型に喋らせ、型に計算させる。
そんなワンランク上のTypeScriptライフを、今日から君のプロジェクトでも始めてみてほしい。
それじゃあ、次のコードレビューで最高の型定義を見せてくれるのを楽しみにしているよ。

コメント