集合子集的穷举匹配

时间:2019-03-29 10:10:30

标签: typescript

我对使用“从不”类型的穷举开关/保护套有疑问。

说,我有一组字符串:{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}的大小写。

1 个答案:

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


哇!好的,我希望这有道理并能为您提供一些指导。祝你好运!