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 的遞歸條件類型和模板文字來逐個字元地分離字串。密切注意遞歸深度和編譯時效能,但對於典型的路由用例,該模式是生產就緒的。