我对使用“从不”类型的穷举开关/保护套有疑问。
说,我有一组字符串:{A,B}(字符串可以是任意长单词,并且Set本身可以很大) 对于每个子集(例如{},{A,B}),我想创建一个函数:show:Set => string
前面的伪代码:
function show(subset: Set<string>): string {
switch(subset) {
case Set([]): return "Empty";
case Set([A]): return "This is A";
case Set([B]): return "This is B";
case Set([A, B]): return "Dies ist A und B";
default: assertUnreachable(subset)
}
}
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
是否可以在编译时保证show函数覆盖所有可能的子集? 因此,将C添加到集合{A,B,C}中需要我增加show函数吗?并添加{C},{A,C},{B,C}和{A,B,C}的大小写。
答案 0 :(得分:1)
接下来,我将假设我们的T
类型的Set<T>
始终是string literal类型的某种并集。我假设您将使用--strict
编译器选项。我还将定义这些变量和类型:
const A = "A"; type A = typeof A;
const B = "B"; type B = typeof B;
好的,我喜欢这种问题。这里发生了一些事情。首先,如评论中所述,您不能直接检查Set
是否相等。因此switch
/ case
不适用。相反,这是一个type guard function,您可以用来查看类型Set<T>
的特定集合是否实际上是类型Set<U>
的特定子集,其中U extends T
。
function isSubset<T extends string, U extends T[]>(
set: Set<T>,
...subsetArgs: U
): set is Set<U[number]> {
const otherSet = new Set(subsetArgs);
if (otherSet.size !== set.size) return false;
for (let v of otherSet.values()) {
if (!set.has(v)) return false;
}
return true;
}
您可以根据需要重新实现,但是基本上,您正在检查元素set
中是否存在,并且仅当该元素存在于subsetArgs
中时才可以。使用方法如下:
declare const set: Set<A | B>; // a set whose elements are either A or B
if isSubset(set) { // no extra args, checking for emptiness
set; // narrowed to Set<never>
} else if isSubset(set, A) { // checking for A only
set; // narrowed to Set<A>
}
看看类型保护程序如何在每个子句中缩小set
的范围?而且您使用的是if
/ else
而不是switch
/ case
。
这里的下一个问题是类型Set<A | B>
不会自动扩展为所有可能的子集Set<never> | Set<A> | Set<B> | Set<A | B>
的并集。您需要自己做:
// note how subset is the expanded union of types
function show(subset: Set<never> | Set<A> | Set<B> | Set<A | B>): string {
if (isSubset(subset)) {
return "Empty";
} else if (isSubset(subset, A)) {
return "This is A";
} else if (isSubset(subset, B)) {
return "This is B";
} else if (isSubset(subset, A, B)) {
return "Something in German I guess";
} else {
return assertUnreachable(subset); // no error now
}
}
console.log(show(new Set([A]))); // okay, prints "This is A"
console.log(show(new Set([A, B]))); // okay, prints "Something in German I guess"
console.log(show(new Set([A, B, "C"]))); // compile time error, "C" unexpected
console.log(show(new Set())); // okay, prints "Empty"
为您编译。一种可能的故障是TypeScript编译器将Set<A>
视为Set<A | B>
的子类型(即Set<T>
在T
中是covariant)。这意味着您的类型保护子句的顺序很重要...只要它决定subset
的类型不是Set<A | B>
,它就会将subset
折叠为never
,会导致相反的问题,即编译器认为检查未通过时将其穷举:
function badShow(subset: PowerSetUnion<A | B>): string {
if (isSubset(subset, A, B)) {
return "Something in German I guess";
} else {
return assertUnreachable(subset); // no error!!!
}
}
如果您仔细检查从最窄到最宽的部分,就可以了。否则,您实际上必须做某种疯狂的事情来强制类型系统遵守:
type InvariantSet<T> = {
set: Set<T>;
"**forceInvariant**": (t: T) => T;
}
如果将所有Set<T>
包装在InvariantSet<T>
对象中,那么它将防止这种子类型关系的发生... "**forceInvariant**"
属性是一个带有T
的函数参数和返回类型。由于TypeScript的contravariance of function parameters,这将限制编译器将InvariantSet<T>
视为InvariantSet<U>
的子类型或父类(如果U extends T
不作为)。现在,我们包装好一切,问题就解决了:
function isSubset<U extends string[]>(set: any, ...subset: U): set is InvariantSet<U[number]> {
const otherSet = new Set(subset);
if (otherSet.size !== set.set.size) return false;
for (let v of otherSet.values()) {
if (!set.set.has(v)) return false;
}
return true;
}
function show(subset: Set<A | B>): string {
const s = { set: subset } as
InvariantSet<never> | InvariantSet<A> | InvariantSet<B> | InvariantSet<A | B>;
if (isSubset(s, A, B)) {
return "Something in German I guess";
} else if (isSubset(s)) {
return "Empty";
} else if (isSubset(s, A)) {
return "This is A";
} else if (isSubset(s, B)) {
return "This is B";
} else {
return assertUnreachable(s);
}
}
function badShow(subset: Set<A | B>): string {
const s = { set: subset } as
InvariantSet<never> | InvariantSet<A> | InvariantSet<B> | InvariantSet<A | B>;
if (isSubset(s, A, B)) {
return "Something in German I guess";
} else {
return assertUnreachable(s); // error as desired!
}
}
以更大的复杂度为代价。因此,不确定那是否是您想要的。
最后,也许您还希望能够采用A | B
之类的联合类型,并使编译器自动吐出Set<never> | Set<A> | Set<B> | Set<A | B>
之类的所有可能子集类型。这就是所谓的power set。
好消息:这是可能的。我之所以说“某种”,是因为这样的天真的定义是循环的,即not supported。相反,我将循环定义展开为一堆单独的但几乎相同的定义,如果递归变得太深,它们将正式退出。这意味着它将支持最多固定数目的选民联合。这可能很好,因为如果您的联合包含成分,则幂集将具有2 成分。我不想处理512或1024个成分的并集,因此我将在9或10级深度处退出递归。在这里:
type PowerSetUnion<U, V = U> = Set<U> | (V extends any ? (PSU0<Exclude<U, V>>) : never);
type PSU0<U, V = U> = Set<U> | (V extends any ? (PSU1<Exclude<U, V>>) : never);
type PSU1<U, V = U> = Set<U> | (V extends any ? (PSU2<Exclude<U, V>>) : never);
type PSU2<U, V = U> = Set<U> | (V extends any ? (PSU3<Exclude<U, V>>) : never);
type PSU3<U, V = U> = Set<U> | (V extends any ? (PSU4<Exclude<U, V>>) : never);
type PSU4<U, V = U> = Set<U> | (V extends any ? (PSU5<Exclude<U, V>>) : never);
type PSU5<U, V = U> = Set<U> | (V extends any ? (PSU6<Exclude<U, V>>) : never);
type PSU6<U, V = U> = Set<U> | (V extends any ? (PSU7<Exclude<U, V>>) : never);
type PSU7<U, V = U> = Set<U> | (V extends any ? (PSU8<Exclude<U, V>>) : never);
type PSU8<U, V = U> = Set<U> | (V extends any ? (PSU9<Exclude<U, V>>) : never);
type PSU9<U, V = U> = Set<U> | (V extends any ? (PSUX<Exclude<U, V>>) : never);
type PSUX<U> = Set<U>; // bail out
这是一堆distributive conditional types,我不知道是否值得解释。让我们看看它对一些具体示例有何作用:
type PowerSetAB = PowerSetUnion<A | B>;
// Set<"A" | "B"> | Set<never> | Set<"A"> | Set<"B">
type PowerSet123 = PowerSetUnion<1 | 2 | 3>;
// Set<never> | Set<1 | 2 | 3> | Set<2 | 3> | Set<3> | Set<2> | Set<1 | 3> | Set<1> | Set<1 | 2>
type PowerSetOhBoy = PowerSetUnion<1 | 2 | 3 | 4 | 5 | 6>;
// Set<never> | Set<1 | 2 | 3 | 4 | 5 | 6> | Set<2 | 3 | 4 | 5 | 6> |
// Set<3 | 4 | 5 | 6> | Set<4 | 5 | 6> | Set<5 | 6> | Set<6> | Set<5> |
// Set<4 | 6> | Set<4> | Set<4 | 5> | ... 52 more ... | Set<...>
对您有用,对吧?无论如何,您都可以将show()
的签名更改为
function show(subset: PowerSetUnion<A | B>): string;
或重写PowerSetUnion<T>
及其子定义以使用InvariantSet<T>
代替Set<T>
并使用它。
哇!好的,我希望这有道理并能为您提供一些指导。祝你好运!