您有一个 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<T, U> 类型采用候选类型T 和可接受值U 的联合(defaults 到BaseVals)。首先,我们检查T extends U 以查看您的候选类型是否是没有空格的值之一。如果是这样,那么ValidVals<T> 就是T。否则,我们检查T extends `${U} ${infer R}` 以查看您的候选类型开始 是否以可接受的值之一,后跟一个空格和更多内容R(我们使用conditional type inference 抓取)。如果不是,那么ValidVals<T> 就是U;我们本质上是说,如果 T 甚至没有以 U 中的某些内容开头,那么“最接近”的可接受值就是 U 本身。
剩下的情况是T 以U 中的某些内容开头,然后是一个空格,然后是更多内容R。我们再次使用条件类型推断来获取实际的初始字符串F(U 是一个联合,但我们想确切知道我们拥有联合的哪个元素。所以F 将成为@987654368 的元素之一@)。这是递归部分的用武之地。在这种情况下,T 看起来像`${F} ${R}`,其中F 是一些可接受的元素,R 是候选类型的其余部分。那么我们输出`${F} ${ValidVals<R, Exclude<U, F>>}`。这意味着输出类型也将以F开头,后跟一个空格,然后是ValidVals<R, Exclude<U, F>>...也就是说,我们将字符串R的其余部分转换为有效类型,其中可接受元素的列表不再包含F 元素(我们使用the Exclude<T, U> utility type 来执行此操作)。
所以对于ValidVals<"apple blargh">,它会检查...是"apple blargh" 在BaseVals 中吗?不。它是否以BaseVals 开头,后跟一个空格?是的!所以我们把"apple" 当作F 和"blargh" 作为R。然后我们评估${ValidVals<R, Exclude<U, F>>},意思是ValidVals<"blargh", "banana" | "orange">,它只评估为"banana" | "orange"。因此ValidVals<"apple blargh"> 变为"apple banana" | "apple orange"。
这里是asVals()辅助函数的实现:
const asVals = <T extends string>(
t: T extends ValidVals<T> ? T : ValidVals<T>
) => t;
很遗憾,您不能写const asVals = <T extends ValidVals<T>>(t: T) => t,因为这是非法循环。相反,我们必须跳过箍让编译器首先推断T,然后使用ValidVals<T> 进行检查。这出于启发式原因我不能在这里深入研究,但基本上如果你在接受类型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<T> 重写为尾递归,但我认为我们不需要在这里进一步讨论。
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