【TypeScript】テンプレートリテラル型 完全ガイド|${Type}構文・Union自動展開・Mapped Types連携・実務パターン徹底解説

【TypeScript】テンプレートリテラル型 完全ガイド|${Type}構文・Union自動展開・Mapped Types連携・実務パターン徹底解説 TypeScript

TypeScript 4.1(2020年11月)で導入されたテンプレートリテラル型は、文字列リテラル型を組み合わせて新しい型を生成する機能です。JavaScript のテンプレートリテラル(`${}`)と同じ構文を型レベルで使い、動的な文字列パターンを型として表現できます。

この機能を使うと、API のルート名・CSS クラス名・イベントハンドラ名など、文字列の命名規則を型で強制できるようになります。本記事では基本から Mapped Types・infer との高度な組み合わせまで、実践パターンを含めて完全解説します。

この記事でわかること

  • テンプレートリテラル型の基本構文と Union 型との自動展開
  • UppercaseLowercaseCapitalizeUncapitalize 組み込み型
  • keyof とテンプレートリテラル型を組み合わせたプロパティ名変換
  • infer で文字列パターンを解析・抽出する方法
  • Mapped Types との連携で setter/getter・イベントハンドラを自動生成する方法
  • 実践パターン3本・FAQ6問
スポンサーリンク

1. テンプレートリテラル型の基本構文

テンプレートリテラル型は `${型}` の形式で書きます。バッククォートと ${} を使うのは JavaScript と同じですが、型コンテキストで使うと型を生成します。

// 基本: 文字列リテラル型の連結
type Greeting = `Hello, ${string}`;
// string で始まるすべての "Hello, " プレフィックス文字列

const g1: Greeting = "Hello, Alice";  // OK
const g2: Greeting = "Hello, 123";    // OK
// const g3: Greeting = "Hi, Alice";  // Error: "Hi, " で始まらない

// リテラル型同士の連結
type Env = "dev" | "staging" | "prod";
type ConfigFile = `${Env}.config.json`;
// "dev.config.json" | "staging.config.json" | "prod.config.json"

// 複数の型パラメータを組み合わせ
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route      = "/users" | "/products" | "/orders";
type ApiEndpoint = `${HttpMethod} ${Route}`;
// "GET /users" | "GET /products" | ... (12 通り) が全て生成される
Union 型は自動的にすべての組み合わせに展開される
テンプレートリテラル型の中に Union 型を使うと、直積(すべての組み合わせ)が自動的に生成されます。2つの Union 型(各N種・M種)を組み合わせると N×M 種の型ができます。大きな Union は型チェックが遅くなることがあるため組み合わせ数に注意してください。

2. 組み込み文字列操作型と as const の活用

2-1. 組み込み文字列操作型(Uppercase / Lowercase / Capitalize)

TypeScript には文字列型を変換する4つの組み込みユーティリティ型があります。これらはテンプレートリテラル型と組み合わせて使うことが多いです。

変換内容
Uppercase<S> 全て大文字に変換 Uppercase<"hello">"HELLO"
Lowercase<S> 全て小文字に変換 Lowercase<"HELLO">"hello"
Capitalize<S> 先頭だけ大文字に変換 Capitalize<"hello">"Hello"
Uncapitalize<S> 先頭だけ小文字に変換 Uncapitalize<"Hello">"hello"
type EventName = "click" | "focus" | "blur" | "change";

// "onClick" | "onFocus" | "onBlur" | "onChange"
type HandlerName = `on${Capitalize<EventName>}`;

// "CLICK" | "FOCUS" | "BLUR" | "CHANGE"
type UpperEvents = Uppercase<EventName>;

// CSS カスタムプロパティ名の型
type ColorScale = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
type ColorName   = "red" | "blue" | "green" | "gray";
type CSSVar = `--color-${ColorName}-${ColorScale}`;
// "--color-red-100" | "--color-red-200" | ... | "--color-gray-900" (36通り)

// 環境変数名のパターン(PREFIX_ + 大文字)
type EnvKey = "database" | "port" | "host";
type EnvVar = `NEXT_PUBLIC_${Uppercase<EnvKey>}`;
// "NEXT_PUBLIC_DATABASE" | "NEXT_PUBLIC_PORT" | "NEXT_PUBLIC_HOST"

2-2. as const と組み合わせる:値から型を自動生成

as const で定数をリテラル型に固定してから、テンプレートリテラル型に渡すことで、値の定義から型を自動生成できます。型と値の二重管理をなくせる実践的なパターンです。

// as const でリテラル型を保持
const METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
const PATHS   = ["/users", "/products", "/orders"] as const;

type Method = typeof METHODS[number]; // "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
type Path   = typeof PATHS[number];   // "/users" | "/products" | "/orders"

// テンプレートリテラル型で組み合わせ
type Endpoint = `${Method} ${Path}`;
// "GET /users" | "GET /products" | ... (15通り) が自動生成

// ルーティングテーブル(存在するエンドポイントのみを許可)
type AllowedEndpoints =
    | "GET /users"
    | "GET /users/:id"
    | "POST /users"
    | "PUT /users/:id"
    | "DELETE /users/:id"
    | "GET /products"
    | "GET /products/:id";

// CSS テーマカラー(as const + テンプレートリテラル型)
const THEME_COLORS = {
    primary:   "#3b82f6",
    secondary: "#6b7280",
    success:   "#22c55e",
    danger:    "#ef4444",
} as const;

type ThemeColor = keyof typeof THEME_COLORS;
// "primary" | "secondary" | "success" | "danger"

type ThemeCSSVar = `--color-${ThemeColor}`;
// "--color-primary" | "--color-secondary" | "--color-success" | "--color-danger"

as const と型推論の詳細は 型推論完全ガイド を参照してください。

3. keyof との組み合わせ:プロパティ名の自動変換

keyof で取得したプロパティ名にテンプレートリテラル型を適用すると、オブジェクトのプロパティ名から派生した新しい型を生成できます。

interface User {
    id: number;
    name: string;
    email: string;
    createdAt: Date;
}

// "getId" | "getName" | "getEmail" | "getCreatedAt"
type GetterNames = `get${Capitalize<string & keyof User>}`;

// "setId" | "setName" | "setEmail" | "setCreatedAt"
type SetterNames = `set${Capitalize<string & keyof User>}`;

// onChange イベントハンドラ名
// "onIdChange" | "onNameChange" | "onEmailChange" | "onCreatedAtChange"
type ChangeHandlers = `on${Capitalize<string & keyof User>}Change`;

// ※ string & keyof T で数値・symbol キーを除外している

この string & keyof T パターンは非常に重要です。keyofstring | number | symbol を含む可能性があるため、string & で文字列キーのみに絞り込みます。

3-1. Mapped Types との組み合わせでオブジェクト型を変換

// getter/setter を持つオブジェクト型を自動生成
type WithGetters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = WithGetters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
//   getCreatedAt: () => Date;
// }

// setter も合わせて定義
type WithSetters<T> = {
    [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

// getter + setter + 元のプロパティを持つ型
type Observable<T> = T & WithGetters<T> & WithSetters<T>;

type ObservableUser = Observable<User>;
// User のプロパティ + getId(), getName(), ... + setId(), setName(), ...

4. infer との組み合わせ:文字列パターンの解析

Conditional Types の infer キーワードとテンプレートリテラル型を組み合わせると、文字列型のパターンマッチングができます。文字列の一部を型として抽出する強力な技術です。

// プレフィックスを除去する型
type RemovePrefix<S extends string, P extends string> =
    S extends `${P}${infer Rest}` ? Rest : S;

type Result1 = RemovePrefix<"onClickHandler", "on">;
// "ClickHandler"

type Result2 = RemovePrefix<"getData", "get">;
// "Data"

// サフィックスを除去する型
type RemoveSuffix<S extends string, Suffix extends string> =
    S extends `${infer Rest}${Suffix}` ? Rest : S;

type Result3 = RemoveSuffix<"getUserById", "ById">;
// "getUser"

// 区切り文字で分割する型
type Split<S extends string, D extends string> =
    S extends `${infer Head}${D}${infer Tail}`
        ? [Head, ...Split<Tail, D>]
        : [S];

type Parts = Split<"a.b.c.d", ".">;
// ["a", "b", "c", "d"]

// kebab-case を camelCase に変換する型
type KebabToCamel<S extends string> =
    S extends `${infer Head}-${infer Tail}`
        ? `${Head}${Capitalize<KebabToCamel<Tail>>}`
        : S;

type Camel1 = KebabToCamel<"background-color">; // "backgroundColor"
type Camel2 = KebabToCamel<"font-size">; // "fontSize"
type Camel3 = KebabToCamel<"border-top-left-radius">; // "borderTopLeftRadius"
再帰型の深さ制限
再帰的なテンプレートリテラル型はコンパイラの再帰深さ制限(デフォルト100程度)に引っかかる場合があります。非常に長い文字列や複雑な再帰は型チェックが遅くなるため、実用範囲での使用を推奨します。

4-1. 文字列から型情報を抽出する実用例

// CSS プロパティ名から値の型を推論
type CSSProperty =
    | "color: string"
    | "font-size: string"
    | "opacity: number"
    | "z-index: number";

type ParseCSSProp<S extends string> =
    S extends `${infer Prop}: ${infer Type}`
        ? { prop: Prop; type: Type }
        : never;

type Parsed = ParseCSSProp<"opacity: number">;
// { prop: "opacity"; type: "number" }

// HTTP メソッドとパスを分離
type ParseRoute<S extends string> =
    S extends `${infer Method} ${infer Path}`
        ? { method: Method; path: Path }
        : never;

type Route = ParseRoute<"GET /api/users">;
// { method: "GET"; path: "/api/users" }

5. 実践パターン3本

実践例1:型安全な国際化(i18n)システム

翻訳キーの階層構造を型で表現し、存在しないキーを参照するとコンパイルエラーになるシステムです。

// 翻訳リソースの型定義
const translations = {
    common: {
        save: "保存",
        cancel: "キャンセル",
        delete: "削除",
    },
    user: {
        profile: "プロフィール",
        settings: "設定",
        logout: "ログアウト",
    },
    errors: {
        notFound: "ページが見つかりません",
        unauthorized: "権限がありません",
    },
} as const;

// ネストしたキーをドット区切りの文字列型として展開
type DotPath<T, Prefix extends string = ""> = {
    [K in keyof T & string]:
        T[K] extends Record<string, unknown>
            ? DotPath<T[K], `${Prefix}${K}.`>
            : `${Prefix}${K}`;
}[keyof T & string];

type TranslationKey = DotPath<typeof translations>;
// "common.save" | "common.cancel" | "common.delete"
// | "user.profile" | "user.settings" | "user.logout"
// | "errors.notFound" | "errors.unauthorized"

// 型安全な翻訳関数
function t(key: TranslationKey): string {
    const [namespace, name] = key.split(".");
    return (translations as any)[namespace][name];
}

t("common.save");        // OK: "保存"
t("user.logout");        // OK: "ログアウト"
// t("user.admin");      // Error: 存在しないキー
// t("common.submit");   // Error: 存在しないキー

実践例2:型安全なイベントシステム(on/off/emit)

テンプレートリテラル型で on${EventName} 形式のハンドラ名を自動生成し、イベント名と型を対応付けるシステムです。

interface FormEvents {
    submit:  { data: Record<string, string> };
    reset:   {};
    change:  { field: string; value: string };
    validate: { field: string; valid: boolean; message?: string };
}

// "onSubmit" | "onReset" | "onChange" | "onValidate"
type EventHandlerProps<Events extends Record<string, unknown>> = {
    [K in keyof Events & string as `on${Capitalize<K>}`]?:
        (event: Events[K]) => void;
};

type FormProps = EventHandlerProps<FormEvents>;
// {
//   onSubmit?:  (event: { data: Record<string, string> }) => void;
//   onReset?:   (event: {}) => void;
//   onChange?:  (event: { field: string; value: string }) => void;
//   onValidate?: (event: { field: string; valid: boolean; message?: string }) => void;
// }

// 使用例(React コンポーネントの props として)
function Form(props: FormProps) {
    return null; // 実装省略
}

<Form
    onSubmit={({ data }) => console.log(data)}    // 型: { data: Record<string,string> }
    onChange={({ field, value }) => {}}           // 型推論で field/value が string
    // onInvalid={...}                            // Error: そのような prop は存在しない
/>

実践例3:Fluent Interface(メソッドチェーン)の型付け

SQL クエリビルダーのようなメソッドチェーンを、テンプレートリテラル型でクエリ文字列そのものを型として表現するパターンです。

// SQL クエリの型安全ビルダー(簡略版)
type Table = "users" | "products" | "orders";
type Column = "id" | "name" | "email" | "price" | "status";
type WhereOp = "=" | "!=" | ">" | "<" | ">=" | "<=";

type SelectQuery = `SELECT ${string} FROM ${Table}`;
type WhereClause = `${Column} ${WhereOp} ${string}`;
type FullQuery   = `${SelectQuery} WHERE ${WhereClause}`;

// 型安全なクエリ関数
function query(sql: SelectQuery | FullQuery): Promise<unknown[]> {
    return fetch("/api/query", { method: "POST", body: JSON.stringify({ sql }) })
        .then(r => r.json());
}

query("SELECT * FROM users");                       // OK
query("SELECT id, name FROM products");             // OK
query("SELECT * FROM orders WHERE status = 'pending'"); // OK
// query("SELECT * FROM comments");                  // Error: "comments" は Table にない
// query("INSERT INTO users VALUES (...)");          // Error: SELECT でない

// CSS-in-JS での型安全なスタイル定義(応用)
type CSSUnit = "px" | "em" | "rem" | "%";
type SizeValue = `${number}${CSSUnit}`;

const fontSize: SizeValue = "16px";    // OK
const margin: SizeValue   = "1.5rem";  // OK
// const width: SizeValue = "100";     // Error: 単位がない
// const height: SizeValue = "auto";   // Error: 数値+単位形式でない

Mapped Types の詳細は 高度な型完全ガイドkeyofinfer の組み合わせは keyof・typeof・インデックスアクセス型ガイド を参照してください。

6. まとめ:使い分け早見表

パターン 構文例 主な用途
文字列リテラル結合 `on${Capitalize<EventName>}` イベントハンドラ名・getter/setter 名の生成
Union の自動展開 `${Method} ${Path}` API エンドポイント・設定キーの全組み合わせ型
as const + typeof + テンプレート `--color-${keyof typeof THEME}` 値の定義から CSS 変数・設定キー型を自動生成
keyof + Mapped Types (as 句) [K in keyof T as `get${Capitalize<...>}`] プロパティ名を変換して新しいオブジェクト型を生成
infer で前後を抽出 S extends `${P}${infer Rest}` プレフィックス除去・文字列分割・パターン解析
再帰的ケース変換 KebabToCamel<S> パターン kebab-case → camelCase など命名規則変換
数値単位型 `${number}px` など CSS 値・バージョン文字列など数値+文字列の型

テンプレートリテラル型は、型定義と実装の二重管理をなくすのに非常に有効です。プロパティ名を追加するだけで getter/setter/イベントハンドラの型が自動的に追従し、補完が効くためタイポによるバグも防げます。 型推論完全ガイド と合わせて活用してください。

FAQ

Qテンプレートリテラル型はどの TypeScript バージョンから使えますか?

ATypeScript 4.1(2020年11月リリース)から使えます。infer との組み合わせや再帰的なテンプレートリテラル型も 4.1 から対応しています。4.2 以降では型推論の精度が向上し、より複雑なパターンも扱えるようになっています。使用前にプロジェクトの tsconfig.json の TypeScript バージョンを確認してください。

Qstring & keyof T と keyof T の違いは何ですか?

Akeyof Tstring | number | symbol を含む場合があります。テンプレートリテラル型に適用できるのは string 型のみのため、string & keyof T で文字列キーのみに絞り込む必要があります。例えば配列型の keyof には number"length" などが含まれますが、string & を付けることで安全にテンプレートリテラル型へ渡せます。

Qテンプレートリテラル型と Mapped Types を組み合わせるときの注意点は?

AMapped Types のキーリマッピング(as 句)でテンプレートリテラル型を使う際、never を返すとそのキーは除外されます。また、Capitalize 等は string 型にしか適用できないため、string & K の形で型を絞り込んでから使うのが鉄則です。型引数が never になっているときは Union が空になっていないか確認してください。

QUnion 型の組み合わせ爆発を避けるにはどうすればよいですか?

A多数の Union 型を組み合わせると型の数が指数的に増え、型チェックが遅くなります。例えば各 5 種の Union を 4 つ組み合わせると 5⁴ = 625 種の型が生成されます。対策として①ジェネリック関数で実行時に組み合わせる②型の代わりに string を使い実行時バリデーションに委ねる③使う組み合わせのみを手動で列挙する、といった方法があります。

Qテンプレートリテラル型で数値を含めたいのですが number 型が使えません。

Aテンプレートリテラル型に number 型を使うと `prefix_${number}` のようにすべての数値文字列にマッチする型になります。これは有効ですが Union には展開されません(無限に存在するため)。特定の数値リテラルのみなら 1 | 2 | 3 のようなリテラル Union を使います。TypeScript 4.8 以降では ${bigint} も使えるようになっています。

Qテンプレートリテラル型とテンプレートリテラル文字列(JS)の関係は?

A構文は同じですが動作するレイヤーが異なります。JavaScript の `Hello, ${name}`実行時に文字列を生成します。TypeScript のテンプレートリテラル型 `Hello, ${string}`コンパイル時に型を生成し、実行時には存在しません。型チェックのみに使われるため、バンドルサイズに影響しません。

テンプレートリテラル型は TypeScript の型システムの表現力を大きく広げる機能です。最初は難しく感じますが、Capitalize<string & keyof T> のパターンから始めて、徐々に infer との組み合わせに挑戦してみてください。