【実務・中級編】 Template Literal Types (テンプレートリテラル型) – TypeScript実践ガイド

こんにちは。フロントエンドチームのコードレビューを見ていると、`string`型やマジックストリートビュー的な文字列結合で溢れかえっているコードに直面して頭を抱えることがよくある。

「APIから返ってくるこのID、プレフィックスが必ず `user_` になるはずなんだけど、型はただの `string` になってるから、うっかり `user_` なしのIDを渡しちゃってバグったわ……」

こういう現場の悲鳴、君も聞いたことがないかい?
TypeScriptを書くうえで、`string` 型という広大な荒野に放り出されたまま開発を続けるのは、目隠しをして高速道路を歩くようなものだ。

今回は、TypeScript 4.1で導入されて以来、実務の型パズルにおいてなくてはならない最強の武器となった 「Template Literal Types(テンプレートリテラル型)」 について、徹底的に解説しよう。
公式ドキュメントをなぞるだけのような退屈な話はしない。明日から君のチームのコードベースを劇的に堅牢にする、実践的なテクニックを授けよう。

—

そもそも Template Literal Types とは何か?

一言で言えば、「JavaScriptのテンプレート文字列(“ `${}` “)の型バージョン」 だ。
文字列リテラル型(`”hello”` や `”world”` など)を組み合わせて、新しい文字列のパターンを型レベルで動的に生成できる。

まずは基本のシンタックスをおさらいしておこう。

type Direction = “top” | “bottom” | “left” | “right”;
type Margin = `margin-${Direction}`;

// 生成される型:
// “margin-top” | “margin-bottom” | “margin-left” | “margin-right”

これだけでも「CSSのユーティリティ型を作るのに便利だな」と思うかもしれないが、真骨頂はここからだ。TypeScriptの真価は、これがユニオン型に対して分配法則(Distributive Conditional Types的な挙動)を自動で行う点にある。
上の例で言えば、4つのユニオン要素それぞれに対してテンプレートリテラルが展開され、4つの組み合わせが自動で生成されている。これが裏側でどう処理されているか、ちょっと頭の片隅に入れておくと複雑な型を書くときに迷わなくなる。

—

なぜブラウザやランタイムではなく「型」で縛るのか?

ここで一歩立ち止まって考えてほしい。
「文字列のフォーマットなんて、どうせランタイムのバリデーション(Zodとか)で弾けばいいじゃん」――そう思ったそこの君、甘い。

TypeScriptの型システムは、ブラウザが実行されるよりはるか手前、つまり「君がエディタでキーボードを叩いているその瞬間」に動作する静的な解析エンジンだ。
V8などのJavaScriptエンジンがコードを解釈する前に、TypeScriptの言語サービス(tsserver)が裏側でこのテンプレートリテラル型を評価し、メモリ上で巨大な文字列の組み合わせツリーを構築している。

つまり、ランタイムエラーになる未来を、コンパイルエラー(=エディタ上の赤波線)という形で極限まで手前に引き剥がすことができる。これが、フロントエンドエンジニアが型を極める最大のメリットだ。

—

【実践】現場で使えるコピペ必須のユースケース3選

理屈はこれくらいにして、明日から使える実用的なコードを見ていこう。今回は中級者へのステップアップとして、少し背伸びした実践的なパターンを3つ紹介する。

1. 厳格なID管理(Type-Safe ID)

実務で一番多いのがこれだ。データベースのIDやDOMの要素IDなど、特定のプレフィックスを強制したい場面は多々ある。

// プレフィックスの定義
type Entity = “user” | “post” | “comment”;

// テンプレートリテラル型でIDの型を厳格に生成
type EntityId = `${T}_${string}`;

type UserId = EntityId<"user">; // “user_${string}”
type PostId = EntityId<"post">; // “post_${string}”

// — 実際の使用例 —
function fetchEntity(id: UserId) {
console.log(`Fetching: ${id}`);
}

// ちゃんと型推論とバリデーションが効く
fetchEntity(“user_12345”); // OK!
// fetchEntity(“post_12345”); // ❌ コンパイルエラー!(PostIdは受け付けない)
// fetchEntity(“12345”); // ❌ コンパイルエラー!(“user_” が抜けている)

「ただの `string`」を撲滅する第一歩として、ID系はすべてこのパターンに置き換えるだけで、結合バグが劇的に減る。

2. デザインシステムのイベントハンドラー型生成

UIコンポーネントのライブラリや、独自のデザイントークンを扱うときに絶大な効果を発揮する。例えば、CSSのプロパティやイベント名を動的に生成する場合だ。

type Size = “sm” | “md” | “lg”;
type Color = “primary” | “secondary” | “danger”;

// “btn-sm-primary” や “btn-lg-danger” などのクラス名を型安全にする
type ButtonClassName = `btn-${Size}-${Color}`;

const applyButtonTheme = (className: ButtonClassName) => {
// DOM操作やクラスの適用
document.getElementById(“my-button”)?.classList.add(className);
};

// 補完が効くし、タイポも完全に防げる
applyButtonTheme(“btn-md-primary”); // OK
// applyButtonTheme(“btn-xl-primary”); // ❌ エラー (“xl” は Size に存在しない)

デザイントークンを変更した際、型定義を修正するだけで、プロジェクト全体で使われている不正なクラス名がすべて赤波線で教えてもらえる。このリファクタリング時の安心感は一度味わうと病みつきになるはずだ。

3. 組み込み型ヘルパー(Uppercase / Lowercase等)との組み合わせ

テンプレートリテラル型には、文字列を操作するための組み込み型(`Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`)が用意されている。これらを組み合わせると、オブジェクトのキーから自動でGetter/Setterの型を生成できる。

type EventName = “click” | “focus” | “blur”;

// イベント名から “on[CapitalizedEventName]” というReact風のProps型を作る
type EventHandlerProps = {
[K in EventName as `on${Capitalize}`]?: (e: Event) => void;
};

/
生成される EventHandlerProps 型:
{
onClick?: (e: Event) => void;
onFocus?: (e: Event) => void;
onBlur?: (e: Event) => void;
}
/

const MyComponent = (props: EventHandlerProps) => {
// コンポーネントの実装
return null;
};

手動で `onClick` や `onFocus` を一つずつ定義する必要はもうない。元となるデータ構造(この場合は `EventName`)が変われば、Props側も勝手に追従してくれる。DRY(Don’t Repeat Yourself)原則の型レベルでの極致だ。

—

シニアからのアドバイス:強すぎる型とどう向き合うか?

ここまでTemplate Literal Typesの素晴らしさを語ってきたが、最後にプロとして一つだけ警告をしておこう。

「型を複雑にしすぎないこと」

テンプレートリテラル型を何段階もネストさせたり、過剰に複雑な条件分岐(Conditional Types)と組み合わせたりすると、TypeScriptのコンパイラ(tsserver)のパフォーマンスが露骨に低下する。エディタの補完が重くなったり、CIでのビルド時間が急増したりする原因の多くは、こうした「魔改造された型」だ。

チームメンバー全員がその型の構造を理解できるか?
ドキュメントなしで、エディタのホバーを見ただけで意図が伝わるか?

ここを常に意識してほしい。優れたアーキテクチャとは、複雑な課題をシンプルに解決する仕組みのことだ。テンプレートリテラル型も、チームの開発体験を上げるための「スパイス」として適材適所で使っていってほしい。

さあ、エディタを開いて、君のプロジェクトのそこら中に転がっている `string` 型を華麗に置き換えに行こうか。

コメント

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