TypeScriptテンプレートリテラル型:型レベル文字列処理の6つのコアパターン
はじめに
TypeScriptプロジェクトで、このような場面に遭遇したことはありませんか?文字列連結の結果が正しい型推論を得られず、ルートパス/users/:idを型レベルで検証できず、CSSクラス名flex items-centerにインテリセンスがなく、イベント名onClickを手動で型宣言する必要がある。これらの課題の根本原因は——TypeScriptの文字列型処理が長らく「広範なstring」レベルにとどまっていたことです。
TypeScript 4.1で導入されたテンプレートリテラル型(Template Literal Type)は、この状況を根本的に変えました。型レベルで文字列連結、パターンマッチング、再帰的解析を可能にし、文字列型安全性を全く新しい高みに押し上げました。本記事では6つのコアパターンを通じて、型レベル文字列処理の技術を習得します。
コア概念クイックリファレンス
| 概念 | 説明 | 例 |
|---|---|---|
| Template Literal Type | テンプレート文字列構文で型を構築 | `on${Capitalize<T>}` |
| Uppercase | 文字列型を大文字に変換 | Uppercase<'hello'> → 'HELLO' |
| Lowercase | 文字列型を小文字に変換 | Lowercase<'HELLO'> → 'hello' |
| Capitalize | 文字列型の先頭を大文字に | Capitalize<'hello'> → 'Hello' |
| Uncapitalize | 文字列型の先頭を小文字に | Uncapitalize<'Hello'> → 'hello' |
| 再帰的型 | 型の自己参照で反復解析を実現 | type Parse<S> = S extends ... ? Parse<...> : ... |
| 条件分配 | ユニオン型が条件型で自動分配 | T extends U ? X : YでTがユニオンの場合、各メンバーを判定 |
| infer | 条件型内で部分型を推論 | S extends ${infer Head}_${infer Tail}`` |
問題分析:文字列型安全性の5つの課題
1. 文字列連結に型推論がない:2つの文字列リテラル型を連結すると、結果型はstringになり、正確なリテラルユニオン型にならない。
2. ルートパスの型が安全でない:/users/:id/posts/:postIdのようなパスパラメータを型レベルで抽出・検証できない。
3. CSSクラス名に検証がない:Tailwind CSSなどのユーティリティクラス名の文字列をコンパイル時にタイポ検出できない。
4. イベント名の型が広すぎる:on + イベント名の組み合わせを手動で宣言する必要があり、イベント型から自動推論できない。
5. 複雑な文字列パターンを解析できない:CSV行やURLクエリパラメータなどの構造化文字列を、型レベルで分解・検証できない。
パターン1:基本テンプレートリテラル連結
type EventName<T extends string> = `on${Capitalize<T>}`
type ClickEvent = EventName<'click'> // 'onClick'
type FocusEvent = EventName<'focus'> // 'onFocus'
type MouseEvent = EventName<'mouseMove'> // 'onMouseMove'
// ユニオン型の自動分配
type DOMEvents = 'click' | 'focus' | 'blur' | 'submit'
type DOMHandlers = EventName<DOMEvents>
// 'onClick' | 'onFocus' | 'onBlur' | 'onSubmit'
// 実践:型安全なイベント登録
function addEventListener<T extends string>(
event: T,
handler: (e: EventName<T>) => void
): void
// CSS BEM命名
type BEM<B extends string, E extends string, M extends string> =
`${B}__${E}${M extends '' ? '' : `--${M}`}`
type ButtonClass = BEM<'btn', 'icon', 'active'> // 'btn__icon--active'
type InputClass = BEM<'input', 'wrapper', ''> // 'input__wrapper'
パターン2:ルートパス型安全性
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? Param
: never
type UserRoutes = ExtractParams<'/users/:id/posts/:postId'>
// 'id' | 'postId'
// 型安全なルートビルダー
type RouteParams<T extends string> = {
[K in ExtractParams<T>]: string
}
function buildRoute<T extends string>(
path: T,
params: RouteParams<T>
): string {
let result: string = path
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value)
}
return result
}
buildRoute('/users/:id/posts/:postId', { id: '1', postId: '42' }) // ✅
buildRoute('/users/:id', { id: '1' }) // ✅
// buildRoute('/users/:id', {}) // ❌ idパラメータが不足
// ルートテーブル型定義
type RouteTable = {
'/': {}
'/users': {}
'/users/:id': { id: string }
'/users/:id/posts/:postId': { id: string; postId: string }
}
function navigate<T extends keyof RouteTable>(
path: T,
...args: RouteTable[T] extends Record<string, never> ? [] : [RouteTable[T]]
): void
パターン3:CSSクラス名型検証
type TailwindSpacing = '0' | '0.5' | '1' | '1.5' | '2' | '3' | '4' | '5' | '6' | '8' | '10' | '12' | '16' | '20' | '24' | '32' | '40' | '48' | '56' | '64'
type TailwindDirection = 't' | 'b' | 'l' | 'r' | 'x' | 'y'
type PaddingClass =
| `p-${TailwindSpacing}`
| `p${TailwindDirection}-${TailwindSpacing}`
| `px-${TailwindSpacing}`
| `py-${TailwindSpacing}`
type MarginClass =
| `m-${TailwindSpacing}`
| `m${TailwindDirection}-${TailwindSpacing}`
| `mx-${TailwindSpacing}`
| `my-${TailwindSpacing}`
type FlexClass = 'flex' | 'flex-row' | 'flex-col' | 'flex-wrap' | 'flex-1' | 'flex-auto' | 'flex-none'
type AlignClass = 'items-start' | 'items-center' | 'items-end' | 'justify-start' | 'justify-center' | 'justify-end' | 'justify-between'
type TailwindClass = PaddingClass | MarginClass | FlexClass | AlignClass | string
// 型安全なclassName
function cx(...classes: TailwindClass[]): string {
return classes.filter(Boolean).join(' ')
}
cx('flex', 'items-center', 'p-4', 'mx-auto') // ✅
cx('flex', 'items-centre', 'p-4') // ⚠️ items-centreはユニオン型にない(stringフォールバックあり)
パターン4:イベント名型推論
type EventMap = {
click: { x: number; y: number }
focus: { relatedTarget: EventTarget | null }
keydown: { key: string; code: string }
change: { value: string }
submit: { formData: FormData }
}
type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void
type EventHandlers = {
[K in keyof EventMap as `on${Capitalize<K & string>}`]: EventHandler<K>
}
// 推論結果
type Result = EventHandlers
// {
// onClick: (event: { x: number; y: number }) => void
// onFocus: (event: { relatedTarget: EventTarget | null }) => void
// onKeydown: (event: { key: string; code: string }) => void
// onChange: (event: { value: string }) => void
// onSubmit: (event: { formData: FormData }) => void
// }
// ジェネリックコンポーネントProps
type ComponentProps<T extends Record<string, unknown>> = {
[K in keyof T as K extends string ? `on${Capitalize<K>}` : never]: (payload: T[K]) => void
}
type ButtonEvents = { click: MouseEvent; hover: MouseEvent }
type ButtonProps = ComponentProps<ButtonEvents>
// { onClick: (payload: MouseEvent) => void; onHover: (payload: MouseEvent) => void }
パターン5:再帰的文字列解析
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S]
type CSVRow = Split<'name,age,city', ','>
// ['name', 'age', 'city']
type Join<T extends string[], D extends string> =
T extends [infer Head extends string, ...infer Tail extends string[]]
? Tail extends []
? Head
: `${Head}${D}${Join<Tail, D>}`
: ''
type Joined = Join<['a', 'b', 'c'], '-'> // 'a-b-c'
// 再帰的URLクエリパラメータ解析
type ParseQuery<S extends string> =
S extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key | keyof ParseQuery<Rest>]: K extends Key ? Value : ParseQuery<Rest>[K] }
: S extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {}
type QueryResult = ParseQuery<'name=alice&age=30&city=beijing'>
// { name: 'alice'; age: '30'; city: 'beijing' }
// 深いパス型安全性
type DeepPath<T, Prefix extends string = ''> =
T extends object
? { [K in keyof T & string]: DeepPath<T[K], Prefix extends '' ? K : `${Prefix}.${K}`> }[keyof T & string]
: Prefix
type Config = {
database: { host: string; port: number }
cache: { ttl: number; maxSize: number }
}
type ConfigPaths = DeepPath<Config>
// 'database.host' | 'database.port' | 'cache.ttl' | 'cache.maxSize'
パターン6:型レベル文字列ユーティリティライブラリ
type TrimStart<S extends string> =
S extends ` ${infer Rest}` ? TrimStart<Rest> : S
type TrimEnd<S extends string> =
S extends `${infer Rest} ` ? TrimEnd<Rest> : S
type Trim<S extends string> = TrimStart<TrimEnd<S>>
type Replace<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${infer Before}${From}${infer After}`
? `${Before}${To}${Replace<After, From, To>}`
: S
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S extends `${infer Head}-${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S
type KebabCase<S extends string> =
S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `${Lowercase<First>}${KebabCase<Rest>}`
: `${First}${KebabCase<Rest>}`
: S
// テスト
type Trimmed = Trim<' hello '> // 'hello'
type Replaced = Replace<'foo-bar-baz', '-', '_'> // 'foo_bar_baz'
type Cameled = CamelCase<'user_name_id'> // 'userNameId'
type Kebabed = KebabCase<'UserNameId'> // 'userNameId' (簡略版)
// 型レベル正規表現マッチング
type MatchPattern<S extends string, Pattern extends string> =
Pattern extends `${infer Before}*${infer After}`
? S extends `${Before}${infer Middle}${After}`
? Middle
: never
: S extends Pattern
? S
: never
type Matched = MatchPattern<'hello-world', 'hello*'>
// 'world'
落とし穴ガイド:5つのよくある罠
1. ❌ 再帰的型に終了条件がない → ✅ 再帰的型には常にbase caseを提供。そうしないとコンパイラがType instantiation is excessively deepをスロー
2. ❌ 実行時にテンプレートリテラル型が動作することを期待 → ✅ テンプレートリテラル型はコンパイル時のみに存在。実行時ロジックは別途実装が必要
3. ❌ ユニオン型展開による組み合わせ爆発 → ✅ ユニオンメンバー数を制限し、`${A}_${B}`が多すぎる組み合わせを生成しないようにする
4. ❌ テンプレートリテラルとanyの混用 → ✅ anyはテンプレートリテラルの推論をショートサーキットする。常にstringまたはリテラル型を使用
5. ❌ TypeScriptの再帰深度制限を無視 → ✅ TSのデフォルト再帰深度は約45レベル。複雑な解析は複数ステップに分割
エラートラブルシューティング:10のよくあるエラー
| エラーメッセージ | 原因 | 解決策 |
|---|---|---|
Type instantiation is excessively deep |
再帰的型が深度制限を超過 | 終了条件を追加または再帰ステップを分割 |
Type produces a tuple type that is too large |
ユニオン型の組み合わせ爆発 | ユニオンメンバー数を削減 |
Parameter has a name but no type |
テンプレートリテラル推論の失敗 | ジェネリック制約を明示的に注釈 |
Type 'string' is not assignable to type 'on${Capitalize}' |
ジェネリックTがリテラルではなくstringと推論 | T extends stringを使用しリテラル型を渡す |
Cannot infer type in template literal |
inferの位置が不適切 | inferの位置を調整し、パターンマッチングが一意になるようにする |
Circular reference detected |
型の自己参照が循環を形成 | 型構造をリファクタリングして循環を解消 |
Type is too complex to represent |
型計算結果が大きすぎる | 型ロジックを簡略化または中間型エイリアスを使用 |
Index signature is missing |
テンプレートリテラルマップのキーがインデックスシグネチャに受け入れられない | Record<template, value>を使用またはインデックスシグネチャを調整 |
Union type too complex |
ユニオン型メンバーが制限を超過 | バッチ処理または条件型でフィルタリング |
Cannot assign template literal type to string |
リテラル型を広範なstringに代入できない | extends string制約または明示的型アサーションを使用 |
高度なテクニック
1. テンプレートリテラル + satisfies:satisfies演算子と組み合わせて、正確な推論を保持しながらテンプレートリテラル型制約を検証。
2. 条件型チェーン解析:複雑な文字列解析を複数の条件型ステップに分割し、各ステップで1つのパターンを処理して、単一レベルの深い再帰を回避。
3. ブランド型 + テンプレートリテラル:テンプレートリテラル型でブランド文字列型を作成。例:type BrandID = `brand_${string}`でIDの誤用を防止。
4. マップ型キーリマッピング:as句でマップ型のキーをテンプレートリテラル変換し、自動イベントハンドラ型推論を実現。
5. コンパイル時テストフレームワーク:Expect<Equal<A, B>>とテンプレートリテラル型で、コンパイル時に型レベル関数の正確性を検証。
比較分析:TSテンプレートリテラル vs Zod文字列 vs io-ts vs ランタイム検証
| 特徴 | TSテンプレートリテラル | Zod文字列 | io-ts | ランタイム検証 |
|---|---|---|---|---|
| コンパイル時検証 | ✅ | ✅(型推論が必要) | ✅(型推論が必要) | ❌ |
| ランタイム検証 | ❌ | ✅ | ✅ | ✅ |
| 文字列パターンマッチング | ✅ テンプレート構文 | ✅ regex | ✅ regex | ✅ regex |
| 再帰的解析 | ✅ 有限深度 | ❌ | ❌ | ✅ 無制限 |
| ゼロランタイムオーバーヘッド | ✅ | ❌ | ❌ | ❌ |
| 型推論精度 | ✅ 非常に高い | ✅ 高い | ✅ 高い | ❌ |
| 学習曲線 | 高い | 低い | 中程度 | 低い |
| エコシステム互換性 | ✅ TSネイティブ | ✅ コミュニティ広範 | ⚠️ やや小規模 | ✅ 無制限 |
オンラインツールおすすめ
- JSONフォーマッター — APIレスポンス型のデバッグ時にJSON構造をフォーマットして確認し、テンプレートリテラル型定義を支援
- 正規表現ツール — テンプレートリテラルマッチングパターンの設計時に、まず正規表現でロジックを検証
- cURL→コード変換ツール — APIリクエストをテンプレートリテラル型付きのTypeScriptコードに変換
まとめと展望
TypeScriptテンプレートリテラル型は、文字列型安全性を「広範なstring」から「正確なリテラルユニオン」へと引き上げ、型レベル文字列処理のパラダイムブレイクスルーを実現しました。6つのコアパターン——基本連結、ルートパス安全性、CSSクラス名検証、イベント名推論、再帰的解析、型レベルユーティリティライブラリ——を通じて、コンパイル時に多数の文字列関連の型エラーを捕捉できます。将来のTypeScriptでは、再帰深度制限の緩和とパターンマッチング能力の強化が期待され、型レベル文字列処理はさらに強力になるでしょう。
関連記事
- TypeScript Template Literal Types — 公式テンプレートリテラル型ドキュメント
- Type Challenges — 高度な型チャレンジ問題集
- Matt Pocock's Template Literal Types Guide — テンプレートリテラル型の深掘りチュートリアル
- TypeScript 4.1 Release Notes — テンプレートリテラル型の初回導入
- Zod String Validation — ランタイム文字列検証リファレンス
ブラウザローカルツールを無料で試す →