【入門編】 Template Literal Typesの再帰的利用 – TypeScript実践ガイド

こんにちは!TypeScriptの型定義の世界へようこそ。チーフアーキテクトの私です。

日々のフロントエンド開発、本当にお疲れ様です。「`string`型や`number`型はなんとなくわかってきたけれど、型で文字をこねくり回すような高度な話になると、途端に頭がクラクラする……」そんな風に感じていませんか? 大丈夫です、安心してください。最初はみんなそこで立ち止まります。

今回は、TypeScriptの型パズルの中でも特にエキサイティングで、実務でもめちゃくちゃ役立つ「Template Literal Types(テンプレートリテラル型)の再帰的利用」について、おしゃべりするような気持ちでゆっくり紐解いていきましょう。

難しい専門用語はなるべく置いておいて、身近な例えからスタートしますよ。

—

そもそも「テンプレートリテラル型」ってなに?

JavaScriptで、バッククォート(“ ` “)を使って変数を文字列に埋め込んだ経験はありますよね? あれの「型バージョン」が、テンプレートリテラル型です。

例えば、「ユーザーID」という文字列が、常に `user_` から始まると決まっている場合、型をこんな風に書けます。

// 「user_」の後に、任意の文字列が続く型
type UserId = `user_${string}`;

const id1: UserId = “user_12345”; // OK!
const id2: UserId = “admin_999”; // エラー!「user_」から始まってないよ!

これだけでも十分便利なんですが、TypeScriptの真骨頂はここからです。「決まったパターンを、終わるまで何回も繰り返し分解・解析したい!」という時に登場するのが、今回主役の「再帰(さいき)」というテクニックです。

—

例え話:マトリョーシカ人形とお買い物のレシート

「再帰」って聞くと、なんだかプログラミングの難解な呪文のように聞こえますよね。でも、安心してください。身近なもので例えてみましょう。

イメージするのは、「マトリョーシカ人形」です。
大きなお人形を開けると、中からちょっと小さなお人形が出てくる。それを開けると、さらに小さなお人形が……。そして、一番中から小さなお人形が出てきたら、そこでパカッと開ける作業は終わりですよね。

あるいは、「お買い物のレシートの品目」でもいいですね。
「りんご、みかん、バナナ」という長い文字列のリストがあったとして、これを1つずつレジでスキャンしていく作業を想像してください。
1. まず「りんご」をスキャンする。
2. 残りの「みかん、バナナ」をまた同じスキャン機に通す。
3. リストが空っぽになったら終了!

この「同じ処理を、終わりが見えるまで自分自身で繰り返す(自分の中に自分を呼び出す)」のが、プログラミングにおける再帰の正体です。これを型(Type)の世界でやってしまうのが、今回のテーマなんです。

—

実践:URLのパスからパラメータを引っこ抜いてみよう!

百聞は一見にしかず。Web開発でよくある「ルーティングのパス解析」を題材にしてみましょう。

例えば、次のようなAPIのパスがあったとします。
`/users/:userId/posts/:postId`

このパスの中から、コロン( `:` )で始まる部分(`userId` や `postId`)を自動的に見つけ出して、「あ、このパスにはこういう変数が必要なんだな」と型安全に教えてくれる仕組みを作りたいのです。

なんだか難しそうですか? 大丈夫、ステップを踏んで一緒にコードを見ていきましょう。エディタを開く準備はいいですか?

1. まずはコードを読んでみましょう

以下のコードを、あなたのTypeScript環境(あるいはPlayground)にそのまま貼り付けてみてください。日本語のコメントを丁寧に書いているので、上から順に読んでいくと、なんとなく空気が伝わるはずです。

/

  • 【ステップ1】
  • 文字列のパスから、パラメータ名(例: “:userId” の “userId” 部分)を
  • 再帰的にゴリゴリと削り出して、ユニオン型(候補の束)として返す魔法の型です。

/
type ExtractParams =
// パスの中に “/” が含まれているか、あるいは全体をスラッシュで分割するイメージ
T extends `${infer Start}/${infer Rest}`
? ExtractParams | ExtractParams // まだ続きがあるなら、自分自身をもう一度呼ぶ(再帰!)
: T extends `:${infer ParamName}`
? ParamName // “:” から始まっていたら、コロンを取った名前をゲット!
: never; // パラメータじゃなければ何もしない

// 実際に試してみましょう!
type MyRoute = “/users/:userId/posts/:postId/comments/:commentId”;

// この型をホバー(マウスカーソルを乗せる)してみてください。
// 驚くべきことに、型は自動的に以下のように推論されます:
// “userId” | “postId” | “commentId”
type ExtractedResult = ExtractParams;

2. コードの裏側を優しく分解

上のコードで使われている、ちょっと見慣れないキーワードを解説しますね。

1. `extends` と `infer`(インファー)

  • これは型世界の「探偵」だと思ってください。
  • `T extends \`${infer Start}/${infer Rest}\“ は、「もし型 `T` が、何か(Start)とスラッシュ(/)と、その残りの何か(Rest)の形に当てはまるなら、StartとRestの正体を突き止めて(inferして)ちょうだい!」という意味です。

2. 再帰呼び出しの仕組み

  • `ExtractParams | ExtractParams` の部分です。
  • マトリョーシカを開けた後、残ったお人形(Rest)をもう一度同じルール(ExtractParams)で分解しています。これを文字列がすり切れる(これ以上パターンに一致しなくなる)まで、TypeScriptのコンパイラが裏で何回も自動で繰り返してくれます。

—

なぜ、こんな面倒くさいことをするの?(実務でのメリット)

「パスからパラメータ名を自動で抜き出せるのは分かったけど、これの何がそんなにすごいの?」って思いますよね。

現場でこれを活かすと、「存在しないURLのパラメータを指定したときに、コンパイルエラーで即座に怒ってくれるルーター」が作れるようになります。

例えば、ReactやVue、あるいはNext.jsなどのモダンなフレームワークでページ遷移の関数を書くときを想像してください。

// パスを受け取り、そのパスに必要なパラメータのオブジェクトを必須にする関数(のイメージ)
function navigateTo(
path: T,
// パスから自動抽出された型を、関数の引数の型として利用する!
params: Record, string>
) {
// 実際のルーティング処理…
console.log(`Navigating to ${path}`, params);
}

// ————————————————–
// 使ってみよう!
// ————————————————–

// OKな例:パスに含まれる userId と postId をちゃんと渡している
navigateTo(“/users/:userId/posts/:postId”, {
userId: “tanaka_123”,
postId: “post_456”,
});

// エラーになる例:
// パスには “:userId” があるのに、パラメータを渡し忘れたり、スペルミスをすると
// TypeScriptが赤く波線を出して優しく教えてくれます!
navigateTo(“/users/:userId/posts/:postId”, {
userId: “tanaka_123”,
// ほら、postIdが足りないよ!って怒ってくれる
});

どうですか? 「文字列のパースなんてサーバーサイドや正規表現の仕事でしょ?」と思われがちですが、TypeScriptの型システムここまでやってくれるんです。開発者のうっかりミス(タイポや引き数忘れ)を、実行する前にエディタの段階で根こそぎ消し去ってくれる、これがテンプレートリテラル型再帰の圧倒的な魅力です。

—

つまずきやすいポイントと「大丈夫だよ」のメッセージ

最後に、この再帰的テンプレートリテラル型を書いていて、誰もが一度はハマる「罠」と、その抜け出し方をお伝えしておきますね。

1. 「無限再帰(Infinite Recursion)」の恐怖

調子に乗って複雑なパターンを書きすぎると、TypeScriptが「おいおい、いつまでループしてんだよ!」と音を上げて、`Type instantiation is excessively deep and possibly infinite.` というエラーを吐くことがあります。

【対策】
大丈夫です、パニックにならないでください。大抵の場合は、再帰の「終わる条件(ベースケース)」がうまく定義できていないか、条件分岐のパターンが網羅されていません。マトリョーシカを開け続けるだけでなく、「これ以上小さくならない最小のサイズになったらやめる」という出口を必ず用意してあげましょう。

2. エディタが重くなる?

型が複雑になればなるほど、TypeScriptの型チェック(Language Server)が少しだけ重たく感じることがあります。
でも、実務で使うときは共通のユーティリティ型として一度ファイル(例えば `utils.ts` や `types.d.ts`)に切り出してしまえば、普段のコンポーネント開発ではその恩恵をただ美味しく受け取るだけになります。恐れずにチームの共通パーツとして育てていきましょう。

—

おわりに

今回は、Template Literal Typesの再帰的利用について、身近な例えを交えながらお話ししました。

最初は「うわ、記号が多くて難しそう……」と引いてしまうかもしれませんが、一つひとつのパーツ(`extends`、`infer`、三項演算子、そして自分を呼ぶ再帰)を分解して眺めてみると、やっていることは案外シンプルで人間らしいルールに基づいています。

TypeScriptの型は、あなたを縛るための「檻」ではなく、あなたの背中をそっと押し、コードの海で迷子にならないように手を引いてくれる「最高に頼れる相棒」です。

今日の記事が、あなたの型パズルに対する苦手意識を少しでも和らげ、「ちょっと書いてみようかな」というワクワクに変わるきっかけになれたなら、これ以上嬉しいことはありません。

それでは、また次回の記事でお会いしましょう! Happy Coding!

コメント

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