文字列の「形」を型レベルで表現できたら、APIのエンドポイントやCSSの値指定で発生するタイプミスをコンパイル時に検出できます。TypeScript 4.1で導入された**テンプレートリテラル型(Template Literal Types)**は、まさにその課題を解決する機能です。
JavaScriptのテンプレートリテラル(バッククォート ` で囲む文字列)と同じ構文を型の世界で使い、文字列リテラル型を動的に合成・分解できます。
テンプレートリテラル型の基本構文
通常のJavaScriptテンプレートリテラルは実行時に文字列を組み立てます。テンプレートリテラル型は、同じバッククォート構文を型定義で使い、コンパイル時に新しい文字列リテラル型を生成します。
type Env = "dev" | "staging" | "prod";
type Endpoint = `https://api-${Env}.example.com`;
// => "https://api-dev.example.com"
// | "https://api-staging.example.com"
// | "https://api-prod.example.com"
${...} に埋め込める型は string、number、bigint、boolean、null、undefined とそれらのリテラル型です。オブジェクト型などは直接埋め込めません。
// OK: プリミティブ型はすべて埋め込み可能
type NumStr = `count-${number}`; // `count-${number}`
type BoolStr = `flag-${boolean}`; // "flag-true" | "flag-false"
// NG: オブジェクト型は不可
// type Bad = `obj-${{ key: string }}`; // コンパイルエラー
ユニオン型との組み合わせ ― 直積展開
テンプレートリテラル型の最大の特徴は、ユニオン型を埋め込むとすべての組み合わせが自動的に生成される点です。数学の直積(デカルト積)と同じ振る舞いをします。
type Size = "sm" | "md" | "lg";
type Color = "red" | "blue" | "green";
type ClassName = `${Size}-${Color}`;
// => "sm-red" | "sm-blue" | "sm-green"
// | "md-red" | "md-blue" | "md-green"
// | "lg-red" | "lg-blue" | "lg-green"
// 3 × 3 = 9通りの型が生成される
複数の補間位置がある場合も同様に掛け合わされます。
type Lang = "en" | "ja";
type Section = "header" | "footer";
type Suffix = "_title" | "_body";
type MessageKey = `${Lang}_${Section}${Suffix}`;
// 2 × 2 × 2 = 8通りの型が生成される
組み合わせ爆発に注意
ユニオンのメンバー数が多くなると、生成される型の数が急激に増加します。TypeScriptには内部的にユニオンメンバー数の上限があり、それを超えるとコンパイルエラーになります。
// 実用的な範囲:数十通り程度に収まるよう設計する
// NG例:100 × 100 = 10,000通り → コンパイルが極端に遅くなる
大規模なユニオンが必要な場合は、ブランド型やカスタムバリデーション関数で代替するのが現実的です。
組み込み文字列操作ユーティリティ型
TypeScript 4.1では、テンプレートリテラル型と組み合わせて使う4種類のIntrinsic String Manipulation Typesが同時に導入されました。これらはコンパイラに直接組み込まれたユーティリティ型で、.d.ts ファイルには定義が存在しません。
| ユーティリティ型 | 変換内容 | 入力例 | 出力例 |
|---|---|---|---|
Uppercase<T> | 全文字を大文字化 | "hello" | "HELLO" |
Lowercase<T> | 全文字を小文字化 | "HELLO" | "hello" |
Capitalize<T> | 先頭1文字のみ大文字化 | "hello" | "Hello" |
Uncapitalize<T> | 先頭1文字のみ小文字化 | "Hello" | "hello" |
type Shouted = Uppercase<"hello world">; // "HELLO WORLD"
type Whispered = Lowercase<"HELLO WORLD">; // "hello world"
type Named = Capitalize<"typescript">; // "Typescript"
type CamelStart = Uncapitalize<"FooBar">; // "fooBar"
ユニオン型を渡すと、各メンバーに対して個別に変換が適用されます。
type Events = "click" | "scroll" | "resize";
type HandlerNames = `on${Capitalize<Events>}`;
// => "onClick" | "onScroll" | "onResize"
実用例: オブジェクトキーからイベントハンドラ名を導出
type DOMEvents = "click" | "focus" | "blur" | "change";
type EventHandlerMap = {
[K in DOMEvents as `on${Capitalize<K>}`]: (event: Event) => void;
};
// => {
// onClick: (event: Event) => void;
// onFocus: (event: Event) => void;
// onBlur: (event: Event) => void;
// onChange: (event: Event) => void;
// }
infer によるパターンマッチと文字列分解
条件型(Conditional Types)の infer キーワードをテンプレートリテラル型と組み合わせると、文字列のパターンマッチングが型レベルで可能になります。正規表現のような柔軟さを型システムで実現する強力な手法です。
基本的なパターンマッチ
// 区切り文字で最初のセグメントを取り出す
type FirstSegment<S extends string> =
S extends `${infer Head}/${infer _Tail}` ? Head : S;
type Result1 = FirstSegment<"api/users/123">; // "api"
type Result2 = FirstSegment<"dashboard">; // "dashboard"
再帰型と組み合わせた文字列変換
TypeScript 4.5以降の末尾再帰最適化により、再帰型が実用的に使えるようになりました。テンプレートリテラル型と組み合わせると、文字列を別のフォーマットへ変換する型を定義できます。
// キャメルケース → ケバブケースへの変換
type CamelToKebab<S extends string> =
S extends `${infer Head}${infer Tail}`
? Head extends Uppercase<Head>
? Head extends Lowercase<Head>
? `${Head}${CamelToKebab<Tail>}`
: `-${Lowercase<Head>}${CamelToKebab<Tail>}`
: `${Head}${CamelToKebab<Tail>}`
: S;
type Kebab = CamelToKebab<"backgroundColor">;
// => "background-color"
type Kebab2 = CamelToKebab<"fontSize">;
// => "font-size"
Split型: 文字列をタプルに分割
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<"2026-02-20", "-">;
// => ["2026", "02", "20"]
type Segments = Split<"api/users/list", "/">;
// => ["api", "users", "list"]
Mapped Types × テンプレートリテラル型 ― キーのリマッピング
TypeScript 4.1で追加された as 句によるキーリマッピングとテンプレートリテラル型を組み合わせると、オブジェクト型のキーを動的に変換できます。
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type UserGetters = Getters<User>;
// => {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
セッター版も同時に定義するパターンもよく使われます。
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserSetters = Setters<User>;
// => {
// setName: (value: string) => void;
// setAge: (value: number) => void;
// setEmail: (value: string) => void;
// }
実務で使える5つの実践パターン
パターン1: 型安全なCSSプロパティ値
type CSSUnit = "px" | "em" | "rem" | "vh" | "vw" | "%";
type CSSLength = `${number}${CSSUnit}`;
function setWidth(el: HTMLElement, width: CSSLength): void {
el.style.width = width;
}
setWidth(document.body, "100px"); // OK
setWidth(document.body, "2.5rem"); // OK
// setWidth(document.body, "100"); // コンパイルエラー: 単位が必要
// setWidth(document.body, "wide"); // コンパイルエラー: 形式が不正
パターン2: 型安全なAPIルーティング
type APIVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type APIRoute = `/${APIVersion}/${Resource}`;
function fetchResource(route: APIRoute): Promise<Response> {
return fetch(`https://api.example.com${route}`);
}
fetchResource("/v1/users"); // OK
fetchResource("/v2/posts"); // OK
// fetchResource("/v3/users"); // コンパイルエラー
パターン3: プロパティ変更イベントの型安全な監視
TypeScript公式ドキュメントでも紹介されている代表的なパターンです。オブジェクトのプロパティ名から "プロパティ名Changed" 形式のイベント名を自動導出し、コールバックの引数型も連動させます。
type PropEventSource<T> = {
on<K extends string & keyof T>(
eventName: `${K}Changed`,
callback: (newValue: T[K]) => void
): void;
};
declare function watch<T>(obj: T): T & PropEventSource<T>;
const config = watch({
host: "localhost",
port: 3000,
debug: false,
});
config.on("hostChanged", (newHost) => {
// newHost は string 型と推論される
console.log(`Host changed to ${newHost}`);
});
config.on("portChanged", (newPort) => {
// newPort は number 型と推論される
console.log(`Port changed to ${newPort}`);
});
// config.on("hosChanged", () => {}); // タイポをコンパイル時に検出
パターン4: i18nの翻訳キーを型で保証
ネストされた翻訳オブジェクトのキーをドット区切りのフラットなパスとして型定義する手法です。
type NestedKeyOf<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? NestedKeyOf<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];
const translations = {
common: {
save: "保存",
cancel: "キャンセル",
},
user: {
profile: {
title: "プロフィール",
edit: "編集",
},
},
} as const;
type TranslationKey = NestedKeyOf<typeof translations>;
// => "common.save" | "common.cancel"
// | "user.profile.title" | "user.profile.edit"
function t(key: TranslationKey): string {
// 翻訳処理
return key; // 簡略化
}
t("common.save"); // OK
// t("common.delet"); // コンパイルエラー: タイポを検出
パターン5: HEXカラーコードの型バリデーション
type HexChar = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7"
| "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"
| "A" | "B" | "C" | "D" | "E" | "F";
type HexColor = `#${HexChar}${HexChar}${HexChar}${HexChar}${HexChar}${HexChar}`;
function setColor(color: HexColor): void {
// ...
}
setColor("#ff00aa"); // OK
setColor("#FF00AA"); // OK
// setColor("#xyz"); // コンパイルエラー: HEXカラー形式ではない
ただし、このパターンでは HexChar の6乗(約1,680万通り)のユニオンが理論上生成されるため、TypeScriptの内部上限に達する可能性があります。実際にはパターンマッチ用の関数シグネチャとして使うか、文字数を減らした簡易版(3桁HEX #${HexChar}${HexChar}${HexChar})を検討してください。
テンプレートリテラル型を使う際の注意点
1. string を埋め込んだ場合は広い型になる
${string} を補間位置に使うと、具体的な文字列リテラルではなくパターンのみを制約する型になります。
type HasPrefix = `data-${string}`;
const a: HasPrefix = "data-testid"; // OK
const b: HasPrefix = "data-"; // OK(空文字列も string に含まれる)
// const c: HasPrefix = "aria-label"; // コンパイルエラー
2. number 埋め込み時の文字列表現
${number} は指数表記 "1e3" のような文字列も受け入れます。一方、"Infinity" や "NaN" は代入できません(GitHub Issue #42996)。
type NumericStr = `${number}`;
const valid1: NumericStr = "42"; // OK
const valid2: NumericStr = "3.14"; // OK
const valid3: NumericStr = "1e3"; // OK(指数表記)
// const invalid1: NumericStr = "Infinity"; // NG: 特殊値は不可
// const invalid2: NumericStr = "NaN"; // NG: 特殊値は不可
// const invalid3: NumericStr = "12_345"; // NG: アンダースコア区切りは不可
3. 型の深さ・複雑度の制限
再帰型には深さの制限があります。TypeScriptのデフォルトでは再帰の深さが一定を超えるとエラーになります。
// 過度に深い再帰は避ける
// type DeepSplit<S> = S extends `${infer H}${infer T}`
// ? [H, ...DeepSplit<T>] : [];
// 数百文字を超える文字列で型エラーが発生する可能性あり
4. テンプレートリテラル型はランタイムバリデーションを代替しない
コンパイル時の型チェックは強力ですが、外部入力(APIレスポンス、ユーザー入力など)に対しては実行時のバリデーションが依然として必要です。型アサーション(as)でテンプレートリテラル型を割り当てても、実行時の安全性は保証されません。
TypeScriptバージョンごとの関連機能
テンプレートリテラル型は単体で完結する機能ではなく、他の型機能と組み合わせることで真価を発揮します。
| バージョン | 追加された関連機能 |
|---|---|
| 4.1(2020年11月) | Template Literal Types、Intrinsic String Manipulation Types、Mapped Typesのキーリマッピング(as句) |
| 4.5(2021年11月) | 末尾再帰の最適化(Tail-Recursion Elimination on Conditional Types) |
| 4.7(2022年5月) | infer に extends 制約を追加可能(infer T extends string) |
| 4.8(2022年8月) | テンプレートリテラル型のナローイング改善 |
| 5.0(2023年3月) | const 型パラメータ |
まとめ
テンプレートリテラル型は、文字列操作を型レベルで実現するTypeScriptの中核機能です。ユニオン型の直積展開、Uppercase などの組み込み変換ユーティリティ、infer によるパターンマッチを組み合わせることで、APIルーティング・イベントシステム・i18nキー管理・CSSプロパティ値など多くの場面で型安全性を高められます。
まずは Capitalize とMapped Typesの as 句の組み合わせから試し、段階的に infer を使った高度なパターンへ進むのがおすすめです。TypeScript Playground(https://www.typescriptlang.org/play)で実際にコードを試しながら、型レベルの文字列操作に慣れていってください。
