TypeScript Template Literal Types完全ガイド〜文字列型操作・実用25パターン【2026年版】〜

TypeScript の Template Literal Types(テンプレートリテラル型)は、文字列リテラル型と型レベル演算を組み合わせて「型として文字列を組み立て・分解・検証する」ことを可能にする強力な機能です。TypeScript 4.1 で導入されて以降、TS 5.x では推論精度・パフォーマンスとも実用段階に達し、ライブラリやアプリケーションコードの「型安全な文字列」を支える中核機能となりました。

本記事では、TypeScript 5.x 準拠で コピペで動く 40+ のコードサンプル を通じて、Template Literal Types の基礎から実務パターン25選、さらに ts-toolbelt 連携・パフォーマンス上の注意まで、文字列型操作に完全特化した形で網羅します。型ガード(TS型ガード完全ガイド)・Mapped Types(TS Mapped Types完全ガイド)・型推論(TS型推論完全ガイド)と組み合わせると効果が最大化するため、合わせて読むと理解が深まります。

  1. 1. Template Literal Types の基礎
    1. 1.1 もっとも単純な例
    2. 1.2 リテラル型を組み込む
    3. 1.3 number / boolean / bigint も埋め込める
  2. 2. ユーティリティ型: Uppercase / Lowercase / Capitalize / Uncapitalize
    1. 2.1 4種類の基本動作
    2. 2.2 union と組み合わせる
    3. 2.3 テンプレートリテラルと組み合わせる
  3. 3. プレフィックス・サフィックス・パス連結
    1. 3.1 プレフィックス付加
    2. 3.2 サフィックス付加
    3. 3.3 パス連結ユーティリティ
    4. 3.4 複数段のパス連結
  4. 4. CSS / DOM 系の型表現
    1. 4.1 CSS の pixel / em / rem 単位
    2. 4.2 CSS プロパティ名の型
    3. 4.3 イベント名(on + Capitalize)
    4. 4.4 Tailwind 風クラス名の型検証
  5. 5. URL / API ルートの型安全化
    1. 5.1 :id のような動的セグメントを抽出
    2. 5.2 抽出したパラメータを Record にする
    3. 5.3 API ルート → ハンドラ型
  6. 6. ケース変換: snake_case ↔ camelCase ↔ PascalCase ↔ kebab-case
    1. 6.1 snake_case → camelCase
    2. 6.2 camelCase → snake_case
    3. 6.3 PascalCase 変換
    4. 6.4 kebab-case ↔ camelCase
    5. 6.5 オブジェクトのキー名を一括変換する
  7. 7. 文字列パースと分解
    1. 7.1 シンプルな Split
    2. 7.2 コマンドライン引数のパース
    3. 7.3 ts-level の parseInt(限定)
    4. 7.4 文字数カウント
  8. 8. SQL / GraphQL のクエリパース(型レベル)
    1. 8.1 SQL 風 SELECT 文を型でパースする
    2. 8.2 テーブル列 → 型安全な SELECT
    3. 8.3 GraphQL 風の最小パース
  9. 9. dot.notation → ネスト value
    1. 9.1 パス文字列から型を取り出す
    2. 9.2 全パスを列挙
    3. 9.3 型安全な get 関数
  10. 10. 形式検証: メール / UUID / 日付 / JSON Pointer
    1. 10.1 メールアドレス検証
    2. 10.2 UUID(雛形)検証
    3. 10.3 日付フォーマット YYYY-MM-DD
    4. 10.4 JSON Pointer(/path/to/value)
  11. 11. Mapped Types との連携
    1. 11.1 Key Remapping でゲッター生成
    2. 11.2 セッター生成
    3. 11.3 イベントハンドラ生成
    4. 11.4 「filter」用に never を使ったキー除外
  12. 12. 実践: リファクタリング「stringly typed → strongly typed」
    1. 12.1 Before(string ベタ書き)
    2. 12.2 After(Template Literal Types でガード)
    3. 12.3 さらに「ルート定義」と紐付ける
  13. 13. 型安全なクエリビルダー
    1. 13.1 列名 → SELECT 句
    2. 13.2 WHERE 句のキー検証
    3. 13.3 ORDER BY のカラム + 方向
  14. 14. ts-toolbelt / type-fest 連携
    1. 14.1 type-fest の CamelCase / SnakeCase
    2. 14.2 type-fest の Get(dot path)
    3. 14.3 ts-toolbelt の String.Split
  15. 15. パフォーマンスと「複雑度爆発」への対処
    1. 15.1 再帰深度の上限
    2. 15.2 union 爆発を避ける
    3. 15.3 「型は表現に。検証は実行時に」原則
    4. 15.4 大規模 union は const assertion から導出する
  16. 16. typed-routes ライブラリ風の実装例
    1. 16.1 ルートテーブル定義
    2. 16.2 ルート文字列から method / path を分離
    3. 16.3 ルート名 → params 型
    4. 16.4 型安全な linkTo
    5. 16.5 型安全な fetchRoute
  17. 17. よくあるハマりどころと回避策
    1. 17.1 union を埋め込むと展開されすぎる
    2. 17.2 number は全 number を表す
    3. 17.3 infer の制約を強くする(TS 5.0+)
    4. 17.4 const 文字列を渡してもらう工夫
  18. 18. 学習を進める次のステップ
  19. 19. まとめ

1. Template Literal Types の基礎

Template Literal Types は `...` 構文を「型」のレベルで使う機能です。文字列リテラル型を埋め込むことで、新しい文字列リテラル型を作れます。

1.1 もっとも単純な例

// 文字列リテラル型をそのまま埋め込む
type Hello = `Hello, ${string}`;

const a: Hello = "Hello, World"; // OK
const b: Hello = "Hello, TypeScript"; // OK
// const c: Hello = "Hi, World"; // Error: 'Hi, World' は 'Hello, ${string}' に代入できない

1.2 リテラル型を組み込む

type Greeting = "Hello" | "Hi" | "Hey";
type Name = "Alice" | "Bob";

type Sentence = `${Greeting}, ${Name}!`;
// "Hello, Alice!" | "Hello, Bob!" | "Hi, Alice!" | "Hi, Bob!" | "Hey, Alice!" | "Hey, Bob!"

const s1: Sentence = "Hello, Alice!"; // OK
// const s2: Sentence = "Hello, Carol!"; // Error

Union 型を埋め込むと、すべての組合せがリテラル union として展開されるのがポイントです。これが Template Literal Types の威力の源泉です。

1.3 number / boolean / bigint も埋め込める

type Px = `${number}px`;
type Bool = `is_${boolean}`;
type Big = `${bigint}n`;

const x: Px = "12px"; // OK
const y: Bool = "is_true"; // OK
const z: Big = "100n"; // OK
// const w: Px = "12em"; // Error

2. ユーティリティ型: Uppercase / Lowercase / Capitalize / Uncapitalize

TS にはテンプレートリテラル型と密接に連携する 4 つの「内在的な文字列操作型」が組み込まれています。

2.1 4種類の基本動作

type A = Uppercase;     // "HELLO"
type B = Lowercase;     // "hello"
type C = Capitalize;    // "Hello"
type D = Uncapitalize;  // "hello"

2.2 union と組み合わせる

type Methods = "get" | "post" | "put" | "delete";
type UpperMethods = Uppercase;
// "GET" | "POST" | "PUT" | "DELETE"

type CapMethods = Capitalize;
// "Get" | "Post" | "Put" | "Delete"

2.3 テンプレートリテラルと組み合わせる

type EventName = `on${Capitalize}`;

type Click = EventName;   // "onClick"
type Change = EventName; // "onChange"
type Focus = EventName;   // "onFocus"

3. プレフィックス・サフィックス・パス連結

3.1 プレフィックス付加

type WithPrefix

= `${P}${T}`; type ApiPaths = WithPrefix; // "/api/users" | "/api/posts" | "/api/comments" const path: ApiPaths = "/api/users"; // OK

3.2 サフィックス付加

type WithSuffix = `${T}${S}`;

type JsFiles = WithSuffix;
// "index.ts" | "main.ts" | "config.ts"

3.3 パス連結ユーティリティ

type Join = A extends "" ? B : B extends "" ? A : `${A}${Sep}${B}`;

type P1 = Join;        // "users/profile"
type P2 = Join;             // "a.b"
type P3 = Join;                // "root"

3.4 複数段のパス連結

type JoinAll =
  T extends readonly [infer Head extends string, ...infer Rest extends string[]]
    ? Rest extends readonly []
      ? Head
      : `${Head}${Sep}${JoinAll}`
    : "";

type Url = JoinAll;
// "https://example.com/users/42"

4. CSS / DOM 系の型表現

4.1 CSS の pixel / em / rem 単位

type Px = `${number}px`;
type Em = `${number}em`;
type Rem = `${number}rem`;
type CssSize = Px | Em | Rem | "auto" | "inherit";

function setWidth(value: CssSize) { /* ... */ }

setWidth("12px"); // OK
setWidth("1.5rem"); // OK
setWidth("auto"); // OK
// setWidth("12"); // Error

4.2 CSS プロパティ名の型

type CssVar = `--${T}`;
type Custom = CssVar;
// "--primary" | "--secondary" | "--accent"

const v: Custom = "--primary"; // OK

4.3 イベント名(on + Capitalize)

type DomEvents = "click" | "change" | "focus" | "blur" | "submit";
type HandlerProps = {
  [K in DomEvents as `on${Capitalize}`]?: (event: Event) => void;
};
// { onClick?: ...; onChange?: ...; onFocus?: ...; onBlur?: ...; onSubmit?: ... }

const handlers: HandlerProps = {
  onClick: (e) => console.log("clicked"),
  onChange: (e) => console.log("changed"),
};

4.4 Tailwind 風クラス名の型検証

type Shade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
type Color = "red" | "blue" | "green" | "gray";
type TailwindColor = `${"bg" | "text" | "border"}-${Color}-${Shade}`;

const c1: TailwindColor = "bg-red-500"; // OK
const c2: TailwindColor = "text-blue-700"; // OK
// const c3: TailwindColor = "bg-purple-500"; // Error

5. URL / API ルートの型安全化

5.1 :id のような動的セグメントを抽出

type ExtractParams =
  Path extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams
    : Path extends `${string}:${infer Param}`
      ? Param
      : never;

type P = ExtractParams;
// "userId" | "postId"

5.2 抽出したパラメータを Record にする

type ParamsObject = {
  [K in ExtractParams]: string;
};

type UserPostParams = ParamsObject;
// { userId: string; postId: string }

function buildUrl

(path: P, params: ParamsObject

): string { let result: string = path; for (const [k, v] of Object.entries(params)) { result = result.replace(`:${k}`, v); } return result; } buildUrl("/users/:userId/posts/:postId", { userId: "1", postId: "10" }); // buildUrl("/users/:userId", { userId: "1", wrong: "x" }); // Error

5.3 API ルート → ハンドラ型

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Routes = "/users" | "/users/:id" | "/posts" | "/posts/:id/comments";

type Handler = (
  req: { params: ParamsObject; body: unknown },
  res: { json: (data: unknown) => void }
) => void;

type RouteTable = {
  [M in HttpMethod]?: {
    [P in Routes]?: Handler

; }; }; const routes: RouteTable = { GET: { "/users/:id": (req) => { // req.params.id は string と推論される console.log(req.params.id); }, }, };

6. ケース変換: snake_case ↔ camelCase ↔ PascalCase ↔ kebab-case

6.1 snake_case → camelCase

type SnakeToCamel =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<SnakeToCamel>}`
    : S;

type T1 = SnakeToCamel; // "userFirstName"
type T2 = SnakeToCamel;      // "createdAt"
type T3 = SnakeToCamel;              // "id"

6.2 camelCase → snake_case

type CamelToSnake =
  S extends `${infer Head}${infer Tail}`
    ? Head extends Uppercase
      ? `_${Lowercase}${CamelToSnake}`
      : `${Head}${CamelToSnake}`
    : S;

type S1 = CamelToSnake; // "user_first_name"
type S2 = CamelToSnake;     // "created_at"

6.3 PascalCase 変換

type SnakeToPascal = Capitalize<SnakeToCamel>;

type P1 = SnakeToPascal; // "UserProfile"
type P2 = SnakeToPascal; // "HttpRequest"

6.4 kebab-case ↔ camelCase

type KebabToCamel =
  S extends `${infer Head}-${infer Tail}`
    ? `${Head}${Capitalize<KebabToCamel>}`
    : S;

type K1 = KebabToCamel; // "dataSourceId"

type CamelToKebab =
  S extends `${infer Head}${infer Tail}`
    ? Head extends Uppercase
      ? `-${Lowercase}${CamelToKebab}`
      : `${Head}${CamelToKebab}`
    : S;

type K2 = CamelToKebab; // "data-source-id"

6.5 オブジェクトのキー名を一括変換する

type CamelizeKeys = {
  [K in keyof T as K extends string ? SnakeToCamel : K]: T[K];
};

type ApiUser = {
  user_id: number;
  first_name: string;
  created_at: string;
};

type ClientUser = CamelizeKeys;
// { userId: number; firstName: string; createdAt: string }

7. 文字列パースと分解

7.1 シンプルな Split

type Split =
  S extends `${infer Head}${Sep}${infer Tail}`
    ? [Head, ...Split]
    : [S];

type Parts = Split; // ["a", "b", "c", "d"]
type Words = Split; // ["hello", "world", "TS"]

7.2 コマンドライン引数のパース

type ParseCmd =
  S extends `${infer Cmd} ${infer Rest}`
    ? { cmd: Cmd; args: Split }
    : { cmd: S; args: [] };

type C1 = ParseCmd;
// { cmd: "build"; args: ["--watch", "--verbose"] }

7.3 ts-level の parseInt(限定)

type Digits = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";

type IsDigitString =
  S extends `${Digits}${infer R}`
    ? R extends ""
      ? true
      : IsDigitString
    : false;

type N1 = IsDigitString;  // true
type N2 = IsDigitString;  // false

7.4 文字数カウント

type LengthOf =
  S extends `${string}${infer Rest}`
    ? LengthOf
    : Acc["length"];

type L1 = LengthOf; // 5
type L2 = LengthOf;      // 0

8. SQL / GraphQL のクエリパース(型レベル)

8.1 SQL 風 SELECT 文を型でパースする

type TrimLeft = S extends ` ${infer R}` ? TrimLeft : S;
type TrimRight = S extends `${infer R} ` ? TrimRight : S;
type Trim = TrimLeft<TrimRight>;

type ParseSelect =
  Q extends `SELECT ${infer Cols} FROM ${infer Table}`
    ? { columns: Split<Trim, ", ">; table: Trim }
    : never;

type Q1 = ParseSelect;
// { columns: ["id", "name", "email"]; table: "users" }

8.2 テーブル列 → 型安全な SELECT

type UsersTable = { id: number; name: string; email: string; age: number };

type SelectQuery = {
  table: string;
  columns: Cols[];
  result: Pick;
};

function select(table: string, cols: Cols[]): SelectQuery {
  return { table, columns: cols, result: {} as Pick };
}

const q = select("users", ["id", "name"]);
// q.result: { id: number; name: string }

8.3 GraphQL 風の最小パース

type ParseGqlField =
  S extends `${infer Field} { ${infer Inner} }`
    ? { field: Trim; selection: Split<Trim, " "> }
    : { field: Trim; selection: [] };

type G1 = ParseGqlField;
// { field: "user"; selection: ["id", "name", "email"] }

9. dot.notation → ネスト value

9.1 パス文字列から型を取り出す

type PathValue =
  Path extends `${infer Head}.${infer Rest}`
    ? Head extends keyof T
      ? PathValue
      : never
    : Path extends keyof T
      ? T[Path]
      : never;

type Profile = {
  user: {
    name: string;
    address: { city: string; zip: number };
  };
};

type V1 = PathValue;          // string
type V2 = PathValue;  // string
type V3 = PathValue;   // number

9.2 全パスを列挙

type Paths = T extends object
  ? {
      [K in keyof T & string]:
        | `${Prefix}${K}`
        | Paths;
    }[keyof T & string]
  : never;

type AllPaths = Paths;
// "user" | "user.name" | "user.address" | "user.address.city" | "user.address.zip"

9.3 型安全な get 関数

function get<T, P extends Paths>(obj: T, path: P): PathValue {
  return path.split(".").reduce((acc: any, key) => acc?.[key], obj);
}

const profile: Profile = {
  user: { name: "Alice", address: { city: "Tokyo", zip: 100 } },
};

const city = get(profile, "user.address.city"); // string
// get(profile, "user.unknown"); // Error

10. 形式検証: メール / UUID / 日付 / JSON Pointer

10.1 メールアドレス検証

type Email = `${string}@${string}.${string}`;

const e1: Email = "alice@example.com"; // OK
// const e2: Email = "no-at-sign"; // Error

10.2 UUID(雛形)検証

type Hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
        | "a" | "b" | "c" | "d" | "e" | "f";

type UuidShape = `${string}-${string}-${string}-${string}-${string}`;

const u1: UuidShape = "550e8400-e29b-41d4-a716-446655440000"; // OK
// 細かい桁数は実行時バリデータ(zod 等)に任せるのが現実的

10.3 日付フォーマット YYYY-MM-DD

type DateString = `${number}-${number}-${number}`;

const d1: DateString = "2026-05-27"; // OK
// const d2: DateString = "2026/05/27"; // Error

10.4 JSON Pointer(/path/to/value)

type JsonPointer =
  P extends `/${infer Head}/${infer Rest}`
    ? Head extends keyof T
      ? JsonPointer
      : never
    : P extends `/${infer Last}`
      ? Last extends keyof T
        ? T[Last]
        : never
      : never;

type Doc = { user: { profile: { age: number } } };
type Age = JsonPointer; // number

11. Mapped Types との連携

11.1 Key Remapping でゲッター生成

type Getters = {
  [K in keyof T as `get${Capitalize}`]: () => T[K];
};

type User = { name: string; age: number };
type UserGetters = Getters;
// { getName: () => string; getAge: () => number }

11.2 セッター生成

type Setters = {
  [K in keyof T as `set${Capitalize}`]: (value: T[K]) => void;
};

type UserSetters = Setters;
// { setName: (value: string) => void; setAge: (value: number) => void }

11.3 イベントハンドラ生成

type EventHandlers = {
  [K in T as `on${Capitalize}`]: (event: { type: K }) => void;
};

type AppEvents = EventHandlers;
// { onLogin: (e: { type: "login" }) => void; onLogout: ...; onError: ... }

11.4 「filter」用に never を使ったキー除外

type RemovePrivate = {
  [K in keyof T as K extends `_${string}` ? never : K]: T[K];
};

type Raw = { id: number; name: string; _internal: boolean; _cache: unknown };
type Public = RemovePrivate; // { id: number; name: string }

12. 実践: リファクタリング「stringly typed → strongly typed」

「文字列で表現されているが、本来は限定された候補しか取らない値」を string から リテラル union や Template Literal Types に置き換えると、IDE 補完・タイプチェック・リファクタ耐性が一気に向上します。

12.1 Before(string ベタ書き)

// Before
function fetchUser(method: string, path: string) {
  // method に "GETT" などタイポしてもコンパイルエラーにならない
  return fetch(path, { method });
}

fetchUser("GETT", "/api/users"); // 通ってしまう

12.2 After(Template Literal Types でガード)

// After
type Method = "GET" | "POST" | "PUT" | "DELETE";
type ApiPath = `/api/${string}`;

function fetchUser(method: Method, path: ApiPath) {
  return fetch(path, { method });
}

fetchUser("GET", "/api/users"); // OK
// fetchUser("GETT", "/api/users"); // Error: 'GETT' は Method ではない
// fetchUser("GET", "/users"); // Error: '/users' は '/api/${string}' ではない

12.3 さらに「ルート定義」と紐付ける

const routes = {
  "GET /api/users": { /* ... */ },
  "POST /api/users": { /* ... */ },
  "GET /api/users/:id": { /* ... */ },
} as const;

type RouteKey = keyof typeof routes; // "GET /api/users" | "POST /api/users" | ...

function call(route: RouteKey) { /* ... */ }

call("GET /api/users"); // OK
// call("PATCH /api/users"); // Error

13. 型安全なクエリビルダー

13.1 列名 → SELECT 句

type Columns = (keyof T & string);

type SelectClause<T, K extends Columns> = `SELECT ${K} FROM ${string}`;

type UsersCols = Columns; // "id" | "name" | "email" | "age"
type Q = SelectClause;
// "SELECT id FROM ${string}" | "SELECT name FROM ${string}"

13.2 WHERE 句のキー検証

type WhereClause = {
  [K in keyof T]?: T[K] | { gt?: T[K]; lt?: T[K]; in?: T[K][] };
};

function where(table: T, clause: WhereClause) { /* ... */ }

where(null as any, { age: { gt: 18 }, name: "Alice" }); // OK
// where(null as any, { unknown: 1 }); // Error

13.3 ORDER BY のカラム + 方向

type OrderBy = `${keyof T & string} ${"ASC" | "DESC"}`;

type O = OrderBy;
// "id ASC" | "id DESC" | "name ASC" | "name DESC" | "email ASC" | ...

function orderBy(clause: OrderBy) { /* ... */ }

orderBy("name ASC"); // OK
// orderBy("unknown ASC"); // Error

14. ts-toolbelt / type-fest 連携

自前で全部書かなくても、ts-toolbelttype-fest といったライブラリには Template Literal Types ベースのユーティリティが豊富に揃っています。実務では「自前」と「ライブラリ」を使い分けます。

14.1 type-fest の CamelCase / SnakeCase

import type { CamelCase, SnakeCase, KebabCase, PascalCase } from "type-fest";

type A = CamelCase;  // "userFirstName"
type B = SnakeCase;    // "user_first_name"
type C = KebabCase;    // "user-first-name"
type D = PascalCase; // "UserFirstName"

14.2 type-fest の Get(dot path)

import type { Get } from "type-fest";

type Conf = { app: { server: { port: number } } };
type Port = Get; // number

14.3 ts-toolbelt の String.Split

import type { String } from "ts-toolbelt";

type Parts = String.Split; // ["a", "b", "c"]

15. パフォーマンスと「複雑度爆発」への対処

Template Literal Types は強力ですが、再帰深度union の組合せ爆発に注意が必要です。

15.1 再帰深度の上限

// TS は型の再帰展開に上限がある(おおむね 1000 ステップ前後)
type RepeatN =
  Acc["length"] extends N ? Acc : RepeatN;

// 巨大 N を渡すと "Type instantiation is excessively deep" エラーになる
// type X = RepeatN; // Error

15.2 union 爆発を避ける

// NG: 5 × 5 × 5 × 5 = 625 通り。型チェックが重くなる
type Heavy = `${"a"|"b"|"c"|"d"|"e"}-${"a"|"b"|"c"|"d"|"e"}-${"a"|"b"|"c"|"d"|"e"}-${"a"|"b"|"c"|"d"|"e"}`;

// OK: 必要なら branded type + 実行時バリデーション(zod 等)に逃がす
type Branded = T & { __brand: Brand };
type SafeStr = Branded;

15.3 「型は表現に。検証は実行時に」原則

import { z } from "zod";

// 型は緩めに、実行時に厳格チェック
type Email = `${string}@${string}.${string}`;

const EmailSchema = z.string().email();

function send(to: Email, body: string) {
  EmailSchema.parse(to); // 実行時にも厳密検証
  // ...
}

15.4 大規模 union は const assertion から導出する

const COLORS = ["red", "blue", "green", "yellow"] as const;
type Color = typeof COLORS[number]; // "red" | "blue" | "green" | "yellow"

// Template Literal で使う場合も同じ
type BgClass = `bg-${Color}`;
// "bg-red" | "bg-blue" | "bg-green" | "bg-yellow"

16. typed-routes ライブラリ風の実装例

最後に、ここまでの要素を組み合わせて「Express / Hono / Next.js などで使える typed-routes」のミニ実装を示します。

16.1 ルートテーブル定義

const routeDefs = {
  getUser: "GET /users/:userId",
  listUsers: "GET /users",
  createUser: "POST /users",
  deleteUser: "DELETE /users/:userId",
} as const;

type RouteDefs = typeof routeDefs;
type RouteName = keyof RouteDefs; // "getUser" | "listUsers" | ...
type RouteStr = RouteDefs[RouteName];

16.2 ルート文字列から method / path を分離

type ParseRoute =
  S extends `${infer M extends HttpMethod} ${infer P}`
    ? { method: M; path: P }
    : never;

type R = ParseRoute;
// { method: "GET"; path: "/users/:userId" }

16.3 ルート名 → params 型

type RouteParams =
  ParamsObject<ParseRoute["path"]>;

type GetUserParams = RouteParams; // { userId: string }
type ListUsersParams = RouteParams; // {}

16.4 型安全な linkTo

function linkTo(
  name: Name,
  params: RouteParams
): string {
  const def = routeDefs[name];
  const [, path] = def.split(" ", 2) as [string, string];
  return path.replace(/:([^/]+)/g, (_, key) => (params as Record)[key] ?? "");
}

linkTo("getUser", { userId: "42" }); // "/users/42"
// linkTo("getUser", {}); // Error: userId required
// linkTo("listUsers", { userId: "x" }); // Error: 余計なキー

16.5 型安全な fetchRoute

async function fetchRoute(
  name: Name,
  ...args: keyof RouteParams extends never
    ? [params?: undefined, init?: RequestInit]
    : [params: RouteParams, init?: RequestInit]
): Promise {
  const [params, init] = args;
  const path = linkTo(name, (params ?? {}) as RouteParams);
  const method = (routeDefs[name].split(" ", 1)[0]) as HttpMethod;
  return fetch(path, { ...init, method });
}

await fetchRoute("getUser", { userId: "1" });
await fetchRoute("listUsers");
// await fetchRoute("getUser"); // Error: params required

17. よくあるハマりどころと回避策

17.1 union を埋め込むと展開されすぎる

// 意図せず巨大 union になっていないか tsc --noEmit で型サイズを観察
type Bad = `${"a"|"b"|"c"|"d"}-${"x"|"y"|"z"}-${"1"|"2"|"3"|"4"|"5"}`;
// 60 通り。必要なものだけに絞るか、Branded で逃がす

17.2 number は全 number を表す

type N = `${number}px`;
// "1px" | "1.5px" | "-3px" | "1e10px" など、ほぼ無限の集合
// 「桁数」を限定したいなら個別リテラル union を使う

type Single = `${0|1|2|3|4|5|6|7|8|9}px`;

17.3 infer の制約を強くする(TS 5.0+)

// TS 5.0+ では infer に extends 制約を付けられる
type FirstNumber =
  S extends `${infer N extends number}${string}` ? N : never;

type X = FirstNumber; // 42 (number 型として推論)

17.4 const 文字列を渡してもらう工夫

// 関数の引数を string で受けると union が崩れる
function bad(s: string) { /* ... */ }

// const 推論を効かせるなら as const か satisfies を呼び出し側で使うか、
// 関数側でジェネリクスにする
function good(s: S): S { return s; }

const v = good("hello"); // "hello" 型として推論される

18. 学習を進める次のステップ

Template Literal Types は 「型レベルプログラミング」の入口でもあります。次の3つを並行して学ぶと、相乗効果で実務スキルが伸びます。

  • TypeScript型ガード完全ガイド — 実行時値の型を絞り込む技法。Template Literal Types で表した「形」を実行時に検証する側を担います。
  • TypeScript Mapped Types完全ガイド — Key Remapping(as句)で本記事の文字列変換と組み合わさり、ゲッター・セッター・イベント等の自動生成が可能になります。
  • TypeScript型推論完全ガイド — Template Literal Types で書いた型がどう推論されるかは、const/as const/satisfies/NoInfer の理解と直結します。

あわせて、本サイトでは TypeScript カテゴリ に基礎から実務まで網羅した記事を揃えています。さらに、型システムをマスターした後のキャリア戦略として、TypeScript を強みにできるエンジニア向けスクールやエージェントを活用するのも有効です。

  • テックアカデミー — TypeScript/React のオンラインコースが豊富で、現役エンジニア講師のマンツーマンサポートが受けられます。
  • 侍エンジニア — 完全マンツーマンで TS の高度な型システムまで踏み込んで学習可能。ポートフォリオ作成に強い。
  • DMM WEBCAMP — TypeScript を含むフルスタック技術を体系的に学べ、転職保証コースも用意されています。
  • レバテックキャリア — TypeScript / React 案件に強い IT エンジニア専門の転職エージェント。Template Literal Types を活かせる開発案件・スカウトが多数。

19. まとめ

本記事では Template Literal Types を以下の観点から解説しました。

  • 基礎: `${...}`構文と union 展開の挙動
  • 標準ユーティリティ: Uppercase/Lowercase/Capitalize/Uncapitalize
  • プレフィックス・サフィックス・パス連結
  • CSS / DOM 系: pixel 単位、CSS 変数、イベント名、Tailwind 風クラス
  • URL ルートの動的セグメント抽出と params 型化
  • ケース変換: snake / camel / pascal / kebab の相互変換
  • パース: Split、コマンドライン、SQL/GraphQL、桁数カウント
  • dot.notation アクセスと Paths 列挙
  • 形式検証: メール、UUID、日付、JSON Pointer
  • Mapped Types との連携でゲッター・セッター・イベント自動生成
  • stringly typed → strongly typed リファクタ
  • クエリビルダー、typed-routes 風実装
  • ts-toolbelt / type-fest 連携
  • パフォーマンスと複雑度爆発への対処

Template Literal Types は単独でも強力ですが、Conditional Types(infer)・Mapped Types(as)・型推論と組み合わせて初めて真価を発揮します。コピペで動く本記事のスニペットを起点に、自分のコードベースの「stringly typed」な箇所を一つずつ「strongly typed」に置き換えていきましょう。型の力で 「コンパイル時に未然に防げるバグ」を一段増やすことが、実務での生産性と安心感に直結します。

コメント

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