集合子集的穷举性匹配

  • 本文关键字:子集 集合 typescript
  • 更新时间 :
  • 英文 :


我有一个关于使用"从不"类型的详尽开关/案例的问题。

比如说,我有一组字符串:{A,B}(字符串可以是任意的长词,集合本身可以非常大) 对于每个子集(如{},{A,B}),我想创建一个函数:show:Set =>字符串

前面的伪代码:

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} 添加大小写。

在下文中,我将假设Set<T>类型中的T将始终是字符串文本类型的某种联合。 我假设您将使用--strict编译器选项。 我还将定义这些变量和类型:

const A = "A"; type A = typeof A;
const B = "B"; type B = typeof B;

好的,我喜欢这种问题。 这里有一些事情发生。 首先,如评论中所述,您不能直接检查Set是否相等。 因此switch/case不适用。 相反,这里有一个类型保护函数,可用于查看一组特定的类型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中是协变的)。 这意味着您的类型保护子句的顺序很重要......一旦它确定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 对函数参数的逆变性,这将限制编译器将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>。 这就是所谓的电源集。

嗯,好消息:这是可能的。 我说"有点"是因为这种事情的幼稚定义是循环的,这是不支持的。 相反,我将把循环定义展开成一堆单独但几乎相同的定义,如果递归太深,这些定义就会正式退出。 这意味着它将支持最多一些固定数量的选民的工会。 这可能没问题,因为如果您的联合包含 n 个成分,那么幂集将有 2n个成分。 我不想处理由 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

这是一堆分发条件类型,我不知道是否值得解释一下。 让我们看看它对一些具体的例子有什么作用:

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>并使用它。


呼! 好的,我希望这是有道理的,并给你一些方向。 祝你好运!

最新更新