Featured image of post 在 TypeScript 类型系统中构建复杂的字符串解析器Featured image of post 在 TypeScript 类型系统中构建复杂的字符串解析器

在 TypeScript 类型系统中构建复杂的字符串解析器

探索 TypeScript 泛型内部的高级递归,以解析字符串文字中的参数。

在类型级别进行解析

TypeScript 的模板文字类型与递归条件类型和 infer 相结合,允许您完全在类型系统内解析和转换字符串文字。这不是一个运行时解析器,它是一个编译时解析器,可以从字符串常量中提取结构,从而为 URL 路由、SQL 查询、命令行参数等启用强类型 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

get("/users/:id", {
  id: "42",
  extra: "x"
});
// Error: 'extra' does not exist in { id: string }

typed-query-parserts-toolbeltHono 等库使用这些技术来提供端到端类型安全,而无需运行时开销。

局限性

问题影响
深度递归限制(~50 级)在很长的琴弦上断裂
没有正则表达式支持无法验证像 /^[a-z]+$/ 这样的模式
没有工会分裂难以解析 `a
编译时成本复杂的解析器会减慢 tsc

概括

类型级解析器在编译时将字符串文字转换为结构化类型。它们支持完全类型化的 URL 路由、查询字符串提取和配置解析,而无需生成代码。使用带有 infer 的递归条件类型和模板文字来逐个字符地分离字符串。密切关注递归深度和编译时性能,但对于典型的路由用例,该模式是生产就绪的。