template-literal-types について
テンプレートリテラル型で構造化文字列を表し、DSLの解析や文字列型の変換を行うTypeScriptスキルです。文字列を多用するAPIにコンパイル時の検査を加えたい開発者向けです。
主な機能
- 01構造化文字列パターンの表現
- 02`infer` による文字列解析
- 03再帰的な文字列型変換
- 04コンパイル時の形式検証
- 05マップ型との組み合わせ
活用シーン
- 01ID、パス、URLの型安全性向上
- 02CSSセレクターやクエリ言語の解析
- 03snake_caseからcamelCaseへの変換
- 04APIの文字列形式の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill template-literal-typesインストール後、Claude Code で/template-literal-typesを入力して読み込まれたか確認します。
詳細
テンプレートリテラル型で DSL と文字列間の関係をモデル化する
概要
テンプレートリテラル型を使うと、JavaScript のテンプレートリテラルの機能を TypeScript の型システムで活用できます。構造化された文字列のサブセットをモデル化したり、ドメイン固有言語(DSL)を解析したり、文字列型間の関係を表現したりできます。条件型や infer キーワードと組み合わせることで、型レベルで高度な文字列操作を行えます。
このスキルは、文字列を多用する API に型安全性をもたらし、強力な型変換を構築するうえで不可欠です。
このスキルを使う場面
- 構造化された文字列パターン(ID、パス、URL)のモデル化
- ドメイン固有言語(CSS セレクター、クエリ言語)の解析
- 文字列型の変換(camelCase、snake_case の変換)
- コンパイル時の文字列形式の検証
- キー変換のためのマップ型との組み合わせ
絶対ルール
テンプレートリテラル型を使って、構造化された文字列のサブセットや DSL をモデル化します。解析には infer を、変換にはマップ型を組み合わせます。
検出
次のような機会に注目します。
// RED FLAGS - Untyped strings that could be precise
type EventType = string; // Could be 'click' | 'hover' | etc.
function query(selector: string): Element; // Could parse CSS selectors
type CSSProperty = string; // Could validate property names
基本的なテンプレートリテラル型
// Match strings starting with a prefix
type PseudoString = `pseudo${string}`;
const science: PseudoString = 'pseudoscience'; // OK
const alias: PseudoString = 'pseudonym'; // OK
const physics: PseudoString = 'physics'; // Error!
// Match specific patterns
type DataAttribute = `data-${string}`;
type HTTPSUrl = `https://${string}`;
type VersionString = `v${number}.${number}.${number}`;
テンプレートリテラルを使ったインデックスシグネチャ
// Allow data-* attributes while keeping type safety
interface Checkbox {
id: string;
checked: boolean;
[key: `data-${string}`]: unknown;
}
const check: Checkbox = {
id: 'subscribe',
checked: true,
'data-listIds': 'all-the-lists', // OK
value: 'yes', // Error: not data-* and not known property
};
infer を使った解析
条件型と infer を使って文字列の一部を抽出します。
// Extract event name from handler type
type EventName<T> = T extends `on${infer Name}` ? Name : never;
type ClickEvent = EventName<'onClick'>; // 'Click'
type HoverEvent = EventName<'onMouseEnter'>; // 'MouseEnter'
type BadEvent = EventName<'handleClick'>; // never
// Extract path parameters
type PathParams<T> = T extends `/users/${infer UserId}/posts/${infer PostId}`
? { userId: UserId; postId: PostId }
: never;
type Params = PathParams<'/users/123/posts/456'>;
// { userId: '123'; postId: '456' }
文字列変換
文字列を変換する再帰型を構築します。
// Convert snake_case to camelCase
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S;
type T1 = CamelCase<'foo'>; // 'foo'
type T2 = CamelCase<'foo_bar'>; // 'fooBar'
type T3 = CamelCase<'foo_bar_baz'>; // 'fooBarBaz'
// Apply to object keys
type CamelCaseKeys<T> = {
[K in keyof T as CamelCase<K & string>]: T[K]
};
type SnakeCase = { user_name: string; email_address: string };
type Camel = CamelCaseKeys<SnakeCase>;
// { userName: string; emailAddress: string }
実例:CSS セレクター
// Enhance querySelector with precise types
type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
interface ParentNode {
// Simple tag selector
querySelector<TagName extends HTMLTag>(
selector: TagName
): HTMLElementTagNameMap[TagName] | null;
// Tag#id selector
querySelector<TagName extends HTMLTag>(
selector: `${TagName}#${string}`
): HTMLElementTagNameMap[TagName] | null;
}
}
// Usage
const img = document.querySelector('img#hero');
// Type: HTMLImageElement | null
// Can access img?.src, img?.alt, etc.
const div = document.querySelector('div#container');
// Type: HTMLDivElement | null
マップ型との組み合わせ
// Create event handler types from event names
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
submit: SubmitEvent;
};
type EventHandlers<Events extends Record<string, Event>> = {
[K in keyof Events as `on${Capitalize<K & string>}`]?:
(event: Events[K]) => void;
};
type Handlers = EventHandlers<EventMap>;
// {
// onClick?: (event: MouseEvent) => void;
// onKeydown?: (event: KeyboardEvent) => void;
// onSubmit?: (event: SubmitEvent) => void;
// }
プレッシャー耐性プロトコル
単純な string 型を使うよう求められた場合:
- パターンを特定する:文字列にはどのような構造がありますか?
- シンプルに始める:まずリテラル型のユニオンを使います
- テンプレートを追加する:無限だが構造化された集合にはテンプレートリテラルを使います
- 解析を検討する:情報の抽出には
inferを使います - エッジケースをテストする:型が単に厳密なだけでなく、正確であることを確認します
注意すべき点
| アンチパターン | 問題点 |
|---|---|
type ID = string | 検証の機会を逃します |
| テストされていない複雑なテンプレート型 | 不正確な可能性があります |
| 逃げ道のない解析 | 複雑なセレクターにはフォールバックが必要です |
| 厳密すぎる型 | 正当なユースケースを壊す可能性があります |
よくある正当化
「string で十分です」
実際には:テンプレートリテラル型を使えば、コンパイル時にタイプミスや無効な形式を検出できます。'user-123' と 'users-123' の違いをすぐに検出できます。
「複雑すぎます」
実際には:まずはシンプルなプレフィックスパターンから始め、必要に応じて複雑さを加えていきます。基本的なテンプレートリテラルにも効果があります。
「パフォーマンスに影響します」
実際には:テンプレートリテラル型はコンパイル時に評価されます。実行時のコストはありません。
クイックリファレンス
| パターン | 構文 | ユースケース |
|---|---|---|
| プレフィックス | `data-${string}` | data 属性 |
| サフィックス | `${string}Event` | イベント名 |
| 中間 | `${string}.${string}` | ファイル拡張子 |
| 抽出 | `on${infer Name}` | 解析 |
| 変換 | `${Head}${Capitalize<Tail>}` | camelCase |
まとめ
テンプレートリテラル型は、文字列を多用するコードに型安全性をもたらします。構造化された文字列のモデル化、DSL の解析、型の変換に活用します。infer やマップ型と組み合わせることで、型レベルで強力な文字列操作を実現できます。
参考資料
- Dan Vanderkam 著『Effective TypeScript』第2版
- 項目54:テンプレートリテラル型を使って DSL と文字列間の関係をモデル化する