お疲れ。最近、君の書くコードを見ていて「お、型定義を頑張ってるな」と感心することが増えた。だがな、APIのルーティング定義や、Sassのユーティリティ型、あるいはフロントエンドで独自のDSL(ドメイン固有言語)を扱い始めた途端に、`string`型でゴリ押ししたり、`any`に逃げたりしていないか?
「文字列の細かいパターンの違いなんて、実行時バリデーションに任せればいいや」――そう思っていた時期が、僕にもありました。だが、モダンなTypeScriptの極致である「Template Literal Typesの再帰的利用(Recursive Template Literal Types)」をマスターすれば、コンパイルの瞬間に文字列の構造を完璧に暴き出し、IDEの補完を神レベルに引き上げることができる。
今回は、実務の現場で「おっ、こいつデキるな」と周囲を唸らせるための、再帰的テンプレートリテラル型の真髄を授けよう。
—
なぜ「再帰」が必要なのか? 基礎のその先へ
TypeScript 4.1で導入されたTemplate Literal Types自体は、もう君も日常的に使っているはずだ。
type EventName = ‘click’ | ‘hover’;
type Action = `on${Capitalize
基本はこれだ。だが、実務で遭遇する「文字列の迷宮」――例えば、`/users/:id/posts/:postId` のようなネストしたURLパスや、ドット区切りのi18nキー(`user.profile.settings.theme`)を型安全に扱いたいとき、固定長のテンプレートリテラルでは太刀打ちできない。パスの深さは可変だからだ。
ここで登場するのが再帰(Recursion)だ。自分の型定義の中で自分自身を呼び出し、文字列を1文字ずつ、あるいはスラッシュやドットなどの区切り文字でパースしながら、TypeScriptのコンパイラに計算させる。
—
ブラウザやコンパイラ裏側の話:TypeScriptは型パズルで何をしているのか?
ちょっと裏側の話をしよう。TypeScriptの型システムは、実は「Turing Complete(チューリング完全)」、つまり理論上はどんな計算でもできる。
私たちが再帰的な型を書いたとき、TypeScriptの型チェッカー(tsc)は裏側で何をやっているか?
それは、「型の評価(Type Evaluation)」という名のパターンのマッチングと展開のループだ。
1. コンパイラは、渡された文字列型を `infer` キーワードを使って「特定のプレフィックス」と「残りの文字列(Rest)」に分解する。
2. 分割したパーツを条件分岐(Conditional Types: `T extends … ? A : B`)にかけ、マッチすれば次の処理へ進む。
3. 再帰呼び出しによって、文字列が終端(`”` や目的の文字)に達するまで、このパターンマッチの木構造(ASTのようなもの)をメモリ上で構築し続ける。
ただし、ここで注意が必要だ。C言語の無限再帰がクラッシュを招くように、TypeScriptの型チェッカーにも無限ループの防衛策がある。現在のTypeScriptでは、最大再帰深度(通常は50階層程度)を超えると、コンパイラは冷酷にエラーを吐き捨てる。「Instantiation depth limit exceeded」ってやつだ。
実務でこれに直面したとき、「あ、俺の再帰、イケナイ深みにハマってるな」と気づけるかどうかが、シニアへの分かれ道になる。
—
現場で即戦力になる実践コード:URLパスパラメータ抽出機
百聞は一見にしかず。実務で一番「おっ」と言われるユースケースとして、「ExpressやReact Router風のパス文字列から、動的パラメータ(`:id`など)を自動抽出して型にする」という処理を書いてみよう。
以下のコードをそのままエディタに貼って、VSCodeのホバーやオートコンプリートの動きを確認してみてほしい。
/
- 【実務レベル】パス文字列から動的パラメータのキーを再帰的に抽出する型
- 例: “/users/:userId/posts/:postId” -> “userId” | “postId”
/
// 1. 文字列をスラッシュや次のセグメントで分解するためのヘルパー型
// 文字列Sを、区切り文字(‘/’)で分割し、先頭と残りに分けるイメージ
type ExtractPathParams
T extends `${string}:${infer Param}/${infer Rest}`
// パラメータが途中にある場合(例: /:id/–)
? Param | ExtractPathParams
: T extends `${string}:${infer Param}`
// パラメータが末尾にある場合(例: /–/:id)
? Param
// パラメータが存在しない場合
: never;
// — 動作確認用のルーティング定義 —
type UserRoute = ‘/api/v1/users/:userId/settings/:settingId’;
type SimpleRoute = ‘/dashboard’;
// 型の検証
type ExtractedUserParams = ExtractPathParams
// 期待される結果: type ExtractedUserParams = “userId” | “settingId”
type ExtractedSimpleParams = ExtractPathParams
// 期待される結果: type ExtractedSimpleParams = never
/
- 応用編:抽出したパラメータのオブジェクトを要求する型安全なルーター関数
/
type RouteParams
[K in ExtractPathParams
};
// 実装例
function navigate
// 実際のルーティング処理(プレースホルダーを実際の値に置換するなど)
let resolvedPath: string = path;
for (const key of Object.keys(params)) {
resolvedPath = resolvedPath.replace(`:${key}`, (params as Record
}
console.log(`Navigating to: ${resolvedPath}`);
return resolvedPath;
}
// === よし、使ってみよう ===
// OK: 正しいパラメータオブジェクトを渡しているためコンパイル通る
navigate(‘/api/v1/users/:userId/settings/:settingId’, {
userId: ‘123’,
settingId: ‘notifications’,
});
// NGコンパイルエラー: “settingId” が足りない、あるいは余計なキーがある
// navigate(‘/api/v1/users/:userId/settings/:settingId’, {
// userId: ‘123’,
// });
どうだ? このコードの美しさは、「URLのパス文字列を変えた瞬間、第2引数の `params` の型が自動で追従する」という点にある。バックエンドのルート定義が変わっても、フロントエンド側で型を手動で書き換える必要はもうない。TypeScriptが勝手にやってくれる。
—
運用時の注意点とアンチパターン
さて、ここまでドヤ顔で再帰的テンプレートリテラル型を紹介したが、現場でこれを運用する上での「ダークサイド」についても言及しておこう。
1. 過剰な複雑化(Over-engineering)
何でもかんでも型でパースしようとするのは悪手だ。例えば、複雑な正規表現のバリデーションをすべて型で再現しようとすると、コンパイル速度(Type Check Time)が目に見えて悪化する。開発中のエディタの動作が重くなったら本末転倒だ。型は「開発者体験(DX)とバグ防止」のためにあるのであって、自己満足のパズルではない。
2. エラーメッセージの解読難易度
再帰が深すぎたり、型定義が破綻したりしたとき、TypeScriptが吐き出すエラーメッセージは時として「呪文」のように読めなくなる。チームメンバーがメンテできる限界の複雑さに留める勇気を持とう。
3. TypeScriptのバージョン依存
テンプレートリテラル型や `infer` の高度な機能は、TypeScriptのバージョンが上がるにつれて進化している。チームの開発環境のバージョン(できれば最新か、少なくともv4.8以降)が揃っていることを確認して導入してくれ。
—
シニアからのまとめ
テンプレートリテラルの再帰的利用は、単なる「テクニック」を超えて、「文字列として散らばっていたドメイン知識を、TypeScriptの型システムに完全に同期させる」ための強力な武器だ。
APIのパス、CSSのモジュール名、多言語化キー、あるいは社内独自のコンポーネント記法など、「あ、これ文字列でハードコーディングしてミスるやつだ」と思った瞬間が、この再帰型を導入するベストタイミングだ。
最初は難しく感じるかもしれないが、まずは今日紹介したコードをコピーし、自分のプロジェクトの適当なファイルに貼り付けて遊んでみてほしい。手を動かせば動かすほど、コンパイラの思考回路が手に取るように分かってくるはずだ。
それじゃあ、今日もイカした型安全なコードを書いていこうぜ。質問があればいつでも声をかけてくれ。

コメント