Typescript 中逗号分隔字符串的真正递归模板文字

Truly recursive Template Literal for comma-separated strings in Typescript

我正在尝试为包含逗号分隔值的字符串定义 Typescript 模板文字。我可以使这个定义真正递归和通用吗?

请参阅 this typescript playground 以试验案例。

每个逗号分隔值代表一个排序顺序,如 height asc。该字符串应定义一个顺序(包括主要、次要、第三等),根据有效字段名称的并集和两个可能的顺序 "asc""desc",可以包含无限多的排序级别,由逗号按照示例代码中的示例。

下面的实现最多处理 4 个排序顺序,但案例 5 表明它并不是真正的递归。当前扩展 (2x2) 的元数只包含最多 4 个可能的值,所以碰巧处理了我尝试过的初始情况。

const FIELD_NAMES = [
  "height",
  "width",
  "depth",
  "time",
  "amaze",
] as const;

const SORT_ORDERS = [
  "asc",
  "desc",
] as const;

type Field = typeof FIELD_NAMES[number];
type Order = typeof SORT_ORDERS[number];

type FieldOrder = `${Field} ${Order}`
type Separated<S extends string> = `${S}${""|`, ${S}`}`;
type Sort = Separated<Separated<FieldOrder>>;

/** SUCCESS CASES */
const sort1:Sort = "height asc"; //compiles
const sort2:Sort = "height asc, depth desc"; //compiles
const sort3:Sort = "height asc, height asc, height asc"; //compiles
const sort4:Sort = "height asc, width asc, depth desc, time asc"; //compiles
const sort5:Sort = "height asc, width asc, depth desc, time asc, amaze desc"; //SHOULD compile but doesn't

/** FAILURE CASES */
const sort6:Sort = "height"; //doesn't compile 
const sort7:Sort = "height asc,"; //doesn't compile
const sort8:Sort = ""; //doesn't compile

我不能再增加这个模板文字的 'arity',因为像下面那样尝试做 2x2x2 会导致 Expression produces a union type that is too complex to represent

type Sort = Separated<Separated<Separated<FieldOrder>>>;

是否可以定义一个模板文字来处理一般情况?

如你所见,template literal types you are creating quickly blow out the compiler's ability to represent unions. If you read the pull request that implements template literal types的那种,你会看到联合类型最多只能有100,000个元素。所以你只能让 Sort 接受最多 4 个逗号分隔值(这将需要大约 11,110 个成员)。而且您当然不能让它接受任意数字,因为那意味着 Sort 需要是一个无限并集,而无限大于 100,000。所以我们不得不放弃将 Sort 表示为特定联合类型的不可能完成的任务。


一般来说, in cases like this is to switch from specific types to generic types which act as recursive constraints。所以我们有 ValidSort<T> 而不是 Sort。如果 T 是有效的排序字符串类型,则 ValidSort<T> 将等同于 T。否则,ValidSort<T> 将是来自 Sort 的一些合理候选者(或这些的并集),它“接近” T

这意味着您打算编写 Sort 的任何地方现在都需要编写 ValidSort<T> 并将一些泛型类型参数添加到适当的范围。此外,除非你想强迫某人写 const s: ValidSort<"height asc"> = "height asc";,否则你需要调用一个 辅助函数 ,类似于 asSort() 检查其输入并推断类型.意思是你得到 const s = asSort("height asc");.

它可能并不完美,但它可能是我们能做的最好的。


让我们看看定义:

type ValidSort<T extends string> = T extends FieldOrder ? T :
  T extends `${FieldOrder}, ${infer R}` ? T extends `${infer F}, ${R}` ?
  `${F}, ${ValidSort<R>}` : never : FieldOrder;

const asSort = <T extends string>(t: T extends ValidSort<T> ? T : ValidSort<T>) => t;

ValidSort<T> 是一个 recursive conditional type,它检查字符串类型 T 以查看它是 FieldOrder 还是以 FieldOrder 开头的字符串后跟逗号和 space。如果它是 FieldOrder,那么我们有一个有效的排序字符串,我们只是 return 它。如果它以 FieldOrder 开头,那么我们递归地检查字符串的其余部分。否则,我们有一个无效的排序字符串,我们 return FieldOrder.

让我们看看实际效果。您的成功案例现在都按预期工作:

/** SUCCESS CASES */
const sort1 = asSort("height asc"); //compiles
const sort2 = asSort("height asc, depth desc"); //compiles
const sort3 = asSort("height asc, height asc, height asc"); //compiles
const sort4 = asSort("height asc, width asc, depth desc, time asc"); //compiles
const sort5 = asSort(
  "height asc, width asc, depth desc, time asc, amaze desc"); //compiles

失败案例失败,错误消息显示“足够接近”的类型,您应该改用:

/** FAILURE CASES */
const sort6 = asSort("height"); // error!
/* Argument of type '"height"' is not assignable to parameter of type 
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" | 
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */

const sort7 = asSort("height asc,"); // error!
/* Argument of type '"height asc,"' is not assignable to parameter of type 
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" | 
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */

const sort8 = asSort(""); // error!
/* Argument of type '""' is not assignable to parameter of type 
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" | 
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */

const sort9 = asSort("height asc, death desc"); // error!
/* Argument of type '"height asc, death desc"' is not assignable to parameter of type 
'"height asc, depth desc" | "height asc, height asc" | "height asc, time asc" |
 "height asc, amaze desc" | "height asc, height desc" | "height asc, width asc" | 
 "height asc, width desc" | "height asc, depth asc" | "height asc, time desc" | 
 "height asc, amaze asc"'. */

我添加了 sort9 来向您展示错误消息如何不仅向您显示 FieldOrder,还向您显示以 "height asc, " 开头后跟 FieldOrder.[=44 的字符串=]

Playground link to code