Featured image of post TypeScriptの型システムで文字列を解析する高度なパーサーの構築 Featured image of post TypeScriptの型システムで文字列を解析する高度なパーサーの構築

TypeScriptの型システムで文字列を解析する高度なパーサーの構築

Template Literal Typesと再帰的推論(infer)を組み合わせ、URLパスの変数定義やJSON文字列から型定義を自動抽出するマニアックな設計例です。

型レベルでの文字列解析

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文字ずつ分解するパターンは、ルーティング用途においてプロダクションでも活用できます。