型レベルでの文字列解析
TypeScript のテンプレートリテラル型(Template Literal Types)と再帰的条件型、infer キーワードを組み合わせると、文字列リテラルを型システム内だけで解析・変換できます。これはランタイムパーサーではなく、コンパイル時に 文字列定数から構造を抽出する仕組みです。URL ルーティング、SQL クエリ、コマンドライン引数など、strongly typed な API を実現します。
テンプレートリテラル型の基礎
テンプレートリテラル型は他の型を補間し、文字列の一部を推論できます。
type Hello<T extends string> = `hello ${T}`;
type Result = Hello<"world">; // "hello world"
条件型の中で infer を使うと、部分文字列を抽出できます:
type ExtractName<T extends string> =
T extends `hello ${infer Name}` ? Name : never;
type A = ExtractName<"hello alice">; // "alice"
type B = ExtractName<"goodbye bob">; // never
URL ルートパラメータの抽出
最も一般的なユースケースは、ルートパターンからパスパラメータを抽出することです:
type RouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof RouteParams<Rest>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = RouteParams<"/users/:id/posts/:postId">;
// { id: string; postId: string }
プロダクション向けルートパーサー
オプショナルセグメントや制約に対応した堅牢なバージョン:
type ParseParam<T extends string> =
T extends `${infer _Prefix}:${infer Param}`
? Param extends `${infer Name}?`
? { [K in Name]?: string }
: { [K in Name]: string }
: {};
type Merge<T, U> = { [K in keyof T | keyof U]: K extends keyof T ? T[K] : K extends keyof U ? U[K] : never };
type ParseRoute<T extends string> =
T extends `${infer Segment}/${infer Rest}`
? Merge<ParseParam<Segment>, ParseRoute<Rest>>
: ParseParam<T>;
オプションパラメータ(?)は結果型のオプショナルプロパティになります。
クエリ文字列の抽出
クエリ文字列も同じパターンで解析できます:
type ParseQuery<T extends string> =
T extends `${infer First}&${infer Rest}`
? Merge<ParseSingle<First>, ParseQuery<Rest>>
: ParseSingle<T>;
type ParseSingle<T extends string> =
T extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};
type Q = ParseQuery<"page=1&limit=10&sort=asc">;
// { page: "1"; limit: "10"; sort: "asc" }
マッピング型と組み合わせて値を変換:
type ParseInt<T extends string> =
T extends `${infer N extends number}` ? N : never;
type TypedQuery = {
[K in keyof Q]: K extends "page" | "limit" ? ParseInt<Q[K]> : Q[K];
};
// { page: 1; limit: 10; sort: "asc" }
API 関数への応用
パーサーを API 関数にバインドすることで、エンドツーエンドの型安全性を実現します:
function get<T extends string>(
path: T,
params: ParseRoute<T>
): void;
get("/users/:id/posts/:postId", {
id: "42",
postId: "99"
}); // OK
Hono、ts-toolbelt、typed-query-parser などのライブラリがこのテクニックを実践的に使用しています。
制限
| 問題 | 影響 |
|---|---|
| 再帰の深さ制限(約50レベル) | 長い文字列で破綻 |
| 正規表現非対応 | /^[a-z]+$/ のようなバリデーション不可 |
| 共用体型の分割が困難 | `a |
| コンパイル時間の増加 | 複雑なパーサーは tsc を遅くする |
まとめ
型レベルパーサーは、文字列リテラルをコンパイル時に構造化された型に変換します。コード生成なしで完全に型付けされた URL ルーティングやクエリ文字列抽出を可能にします。再帰的条件型と infer、テンプレートリテラルを組み合わせて文字列を1文字ずつ分解するパターンは、ルーティング用途においてプロダクションでも活用できます。
