【问题标题】:Truly recursive Template Literal for comma-separated strings in TypescriptTypescript中逗号分隔字符串的真正递归模板文字
【发布时间】:2021-07-14 23:09:40
【问题描述】:

我正在尝试为包含逗号分隔值的字符串定义 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>>>;

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

【问题讨论】:

    标签: typescript csv sorting recursion template-literals


    【解决方案1】:

    如您所见,您创建的template literal types 会迅速破坏编译器表示联合的能力。如果您阅读pull request that implements template literal types,您会发现联合类型最多只能包含 100,000 个元素。所以你只能让Sort 接受最多 4 个逗号分隔的值(这需要大约 11,110 个成员)。而且你当然不能让它接受任意数字,因为这意味着Sort 需要是一个无限联合,并且无限大于 100,000。所以我们不得不放弃将Sort表示为特定联合类型这一不可能完成的任务。


    一般情况下,my approach 在这种情况下是从特定类型切换到充当recursive constraints通用 类型。所以我们有ValidSort&lt;T&gt;,而不是Sort。如果T 是一个有效的排序字符串类型,那么ValidSort&lt;T&gt; 将等价于T。否则,ValidSort&lt;T&gt; 将是来自Sort 的一些合理候选者(或它们的联合),它与T“接近”。

    这意味着您打算编写Sort 的任何地方现在都需要编写ValidSort&lt;T&gt; 并将一些泛型类型参数添加到适当的范围。此外,除非您想强制某人编写const s: ValidSort&lt;"height asc"&gt; = "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&lt;T&gt;recursive conditional type,它检查字符串类型 T 以查看它是 FieldOrder 还是以 FieldOrder 开头的字符串,后跟逗号和空格。如果是FieldOrder,那么我们有一个有效的排序字符串,我们只需返回它。如果它以FieldOrder 开头,那么我们递归地检查字符串的其余部分。否则,我们有一个无效的排序字符串,我们返回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 的字符串。

    Playground link to code

    【讨论】:

    • 谢谢,@jcalz。我推测同样的方法也可以用来解决stackoverflow.com/questions/66298024/… 看看如何使用运行时代码路径来解决打字问题真的很有趣!
    • 为什么是T extends `${FieldOrder}, ${infer R}`?是否可以改为T extends `${FieldOrder}, ${ValidSort&lt;infer R&gt;}`
    • 我想这会达到递归限制,但你总是可以自己尝试。
    • @jcalz 谢谢!知道这是否可以验证嵌套在对象中更深的字符串。例如。验证 foo.bar.baz 是一个具有递归定义的字符串数组?我能想到的最好的办法是要求该数组的每个元素都通过一个像上面这样的函数来确保它是有效的,但想不出一种方法来做例如。 validatedObject({foo: {bar: {baz: ['height asc, death desc', 'width desc', ...]}}})
    • 喜欢this 可能吗?这有点超出了问题的范围,并且评论部分不是回答后续问题的好地方,所以如果您对此有任何其他要问的问题,您可能想发布一个关于它的新问题。跨度>
    猜你喜欢
    • 2021-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-21
    相关资源
    最近更新 更多