【问题标题】:String type that accepts any combination of values separated by spaces接受由空格分隔的任意值组合的字符串类型
【发布时间】:2022-01-07 05:07:53
【问题描述】:

我正在尝试创建一种类型,它可以接受由空格分隔的某些值的任意组合。顺序无关紧要。我无法使用字符串文字定义每个组合,因为可接受值的数量非常长。我怎样才能做到这一点?

type vals = "apple" | "banana" | "orange"; // much longer list

let str1:vals = "apple" // OK
let str2:vals = "banana" // OK
let str3:vals = "orange" // OK
let str4:vals = "apple banana" // OK
let str5:vals = "banana apple" // OK
let str6:vals = "banana orange" // OK
// etc.

let str7:vals = "banana strawberry" // NOT OK

【问题讨论】:

  • @DanielHilgarth 对我来说,这个答案说不支持正则表达式类型检查。所以我只能假设我只能使用一系列联合类型的组合和排列来满足我的需要。但这不切实际,因为可接受的值列表很长,所以我仍然想问是否有任何替代解决方案
  • 如果你只想要单打和双打,你可以type foo = vals | `${vals} ${vals}`;,但如果你想要任意数量的值,那么我认为除了手动列出你可能有多少值之外别无他法大多数。
  • "banana banana" 好吗? “更长的列表”有多长?
  • 似乎与this question几乎重复

标签: typescript


【解决方案1】:

您有一个 BaseVals 类型,由单个单词的 union 组成:

type BaseVals = "apple" | "banana" | "orange" // maybe up to 40 of these

从概念上讲,您希望您的 Vals 类型是由空格分隔的这些单词的子集的所有可能排列的联合。你可以尝试使用template literal types 来生成这些,但不幸的是,TypeScript 只能处理少于大约 100,000 个成员的联合。如果BaseVals 甚至有 8 个成员,那么生成的联合将需要包含 109,600* 个成员。因此,对于您计划的最多 40 名成员的列表,这确实行不通。这意味着 TypeScript 中没有特定类型可以表示 Vals


您可以放弃特定 Vals 类型,而改为编写generic ValidVals<T> 类型,作为候选类型T 的约束。因此,您没有提前列出可能的可接受值的大列表;你正在接受T 并检查它的可接受性。如果可以接受,那么ValidVals<T> 应该只是T。如果不是,那么ValidVals<T> 应该是一个“接近”T 的可接受值。

由于ValidVals<T> 不是特定类型,你真的不想直接用它注释变量,这是多余的(例如,let s: ValidVals<"apple"> = "apple")。相反,您可以编写一个泛型辅助函数并使用它让编译器推断泛型类型(例如,let s = asVals("apple"))。

这是ValidVals的可能实现:

type ValidVals<T extends string, U extends string = BaseVals> = 
  T extends U 
    ? T 
    : T extends `${U} ${infer R}` 
      ? T extends `${infer F} ${R}` 
        ? `${F} ${ValidVals<R, Exclude<U, F>>}` 
        : never 
      : U;

ValidVals&lt;T, U&gt; 类型采用候选类型T 和可接受值U 的联合(defaultsBaseVals)。首先,我们检查T extends U 以查看您的候选类型是否是没有空格的值之一。如果是这样,那么ValidVals&lt;T&gt; 就是T。否则,我们检查T extends `${U} ${infer R}` 以查看您的候选类型开始 是否以可接受的值之一,后跟一个空格和更多内容R(我们使用conditional type inference 抓取)。如果不是,那么ValidVals&lt;T&gt; 就是U;我们本质上是说,如果 T 甚至没有以 U 中的某些内容开头,那么“最接近”的可接受值就是 U 本身。

剩下的情况是TU 中的某些内容开头,然后是一个空格,然后是更多内容R。我们再次使用条件类型推断来获取实际的初始字符串FU 是一个联合,但我们想确切知道我们拥有联合的哪个元素。所以F 将成为@987654368 的元素之一@)。这是递归部分的用武之地。在这种情况下,T 看起来像`${F} ${R}`,其中F 是一些可接受的元素,R 是候选类型的其余部分。那么我们输出`${F} ${ValidVals&lt;R, Exclude&lt;U, F&gt;&gt;}`。这意味着输出类型也将以F开头,后跟一个空格,然后是ValidVals&lt;R, Exclude&lt;U, F&gt;&gt;...也就是说,我们将字符串R的其余部分转换为有效类型,其中可接受元素的列表不再包含F 元素(我们使用the Exclude&lt;T, U&gt; utility type 来执行此操作)。

所以对于ValidVals&lt;"apple blargh"&gt;,它会检查...是"apple blargh"BaseVals 中吗?不。它是否以BaseVals 开头,后跟一个空格?是的!所以我们把"apple" 当作F"blargh" 作为R。然后我们评估${ValidVals&lt;R, Exclude&lt;U, F&gt;&gt;},意思是ValidVals&lt;"blargh", "banana" | "orange"&gt;,它只评估为"banana" | "orange"。因此ValidVals&lt;"apple blargh"&gt; 变为"apple banana" | "apple orange"

这里是asVals()辅助函数的实现:

const asVals = <T extends string>(
  t: T extends ValidVals<T> ? T : ValidVals<T>
) => t;

很遗憾,您不能写const asVals = &lt;T extends ValidVals&lt;T&gt;&gt;(t: T) =&gt; t,因为这是非法循环。相反,我们必须跳过箍让编译器首先推断T,然后使用ValidVals&lt;T&gt; 进行检查。这出于启发式原因我不能在这里深入研究,但基本上如果你在接受类型T extends ... ? ... : ... 的地方传入一个值并要求编译器从中推断T,它猜测该值是输入T 然后检查它。

让我们检查一下它是否有效:

let str1 = asVals("apple") // OK
let str2 = asVals("banana") // OK
let str3 = asVals("orange") // OK
let str4 = asVals("apple banana") // OK
let str5 = asVals("banana apple") // OK
let str6 = asVals("banana orange") // OK

let str7 = asVals("banana strawberry") // NOT OK
// Argument of type '"banana strawberry"' is not assignable to 
// parameter of type '"banana apple" | "banana orange"'.

let str8 = asVals("") // NOT OK
// Argument of type '""' is not assignable to 
// parameter of type 'BaseVals'.

let str9 = asVals("apple apple") // NOT OK
// Argument of type '"apple apple"' is not assignable to
// parameter of type '"apple banana" | "apple orange"'.

万岁!好的例子检查出来,而坏的例子产生看起来合理的错误消息。传入"banana strawberry" 会产生一个关于它如何期望"banana apple""banana orange" 的错误,它们是"banana strawberry" 的“最接近”可接受的类型。这些“最接近”的类型也有助于自动完成。如果你输入"banana " 并自动完成,它会显示"banana apple""banana orange"


请注意,这可能还有一些警告。

最大的问题是:如果您使用的是 TypeScript 4.4 或更低版本,则在大约 25 个级别有一个相当浅的递归限制,因此如果 BaseVals 确实有 40 个元素,并且您调用 asVals(str) 其中 str 是一个由超过 25 个空格分隔的元素组成的字符串文字,您会看到类似“类型实例化过深并且可能无限”的错误。 TypeScript 4.5 在microsoft/TypeScript#45711 中将此限制增加到大约 100 个级别,甚至还做到了,所以如果您在use tail-recursive types only 中可以达到大约 1000 个级别。由于您有 40 个,因此上述版本在 TypeScript 4.5 及更高版本中应该可以正常工作。如果您需要超过 100 个,我们必须将 ValidVals&lt;T&gt; 重写为尾递归,但我认为我们不需要在这里进一步讨论。


Playground link to code

* 8 + 8×7 + 8×7×6 + 8×7×6×5 + 8×7×6×5×4 + 8×7×6×5×4×3 + 8 ×7×6×5×4×3×2 + 8×7×6×5×4×3×2×1 = 109,600

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-17
    • 2018-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多