使用区分联合时,是否可以将一个对象的类型限制为另一个对象的类型?

时间:2019-04-26 09:06:55

标签: typescript types casting static-typing

我有两节课:

interface Num {
    v: number, 
    type: 'num'
}

interface Sum {
    left: Expr, 
    right: Expr,
    type: 'sum'
}

和类型:

type Expr = Sum | Num;

我想检查两个Expr是否相等,但是具有独立功能(与面向对象方法相反)。

我想写些类似的东西:

function isEqual(e1: Expr, e2: Expr): boolean {
    if (e1.type !== e2.type) return false;
    switch(e1.type) {
        case 'num': return e1.v === e2.v;
        case 'sum': return isEqual(e1.left, e2.left) && isEqual(e1.right, e2.right);
    }
}

这是不可能的,因为该功能在开关内部不知道e2的类型。

我需要这样写:

function isEqual(e1: Expr, e2: Expr): boolean {
    if (e1.type !== e2.type) return false;
    switch(e1.type) {
        case 'num': return e1.v === (e2 as Num).v;
        case 'sum': return isEqual(e1.left, (e2 as Sum).left) && isEqual(e1.right, (e2 as Sum).right);
    }
}

那太乱了。

是否有一种方法可以使编译器推断e1函数中e2isEqual的类型(在初始检查之后)必须相等,或者至少具有没有任何显式强制转换的常见超类型?

2 个答案:

答案 0 :(得分:1)

缩小歧视性工会类型仍然很愚蠢。但是,是的,有一种方法,就是您可能会不喜欢它。

function isEqual(e1: Expr, e2: Expr): boolean {
    if (e1.type !== e2.type) return false;
    const t1 = e1.type
    const t2 = e2.type
    switch(e1.type) {
        case 'num': return e1.type === e2.type && e1.v === e2.v;
        case 'sum': return e1.type === e2.type && isEqual(e1.left, e2.left) && isEqual(e1.right, e2.right);
    }
}

要使其稍好一点,请使用自定义类型防护程序

const s = function isSameType<T>(e1: T, e2: any): e2 is typeof e1 {
    return true
}

// then replace `e1.type === e2.type` with `s(e1, e2)`
// ...
    case 'num': return s(e1, e2) && e1.v === e2.v;

我将简要解释其背后的原因。

这里有2个条件,对人类来说很明显,两个条件都可以在运行时缩小类型。

/* PSEUDO CODE */
e1.type !== e2.type => (typeof e1 == typeof e2)
case 'num' => (typeof e1 == Num)

// we as human can tell:
(typeof e1 == typeof e2) & (typeof e1 == Num)
=> typeof e2 == Num

// but TS still see `typeof e2 == Expr`
// not smart enough to make connection btw 2 inferences.

但是,如果您将case放在===之前,TS会告诉您

/* PSEUDO CODE */
case 'num' => (typeof e1 == Num)
e1.type === e2.type => (typeof e1 == typeof e2)
(typeof e1 == Num) & (typeof e1 == typeof e2)
=> typeof e2 == Num

答案 1 :(得分:1)

是的,很抱歉,类型断言可能是获得所需内容的唯一方法。

我只想在这个问题上加上一个问题,问题似乎是编译器未将一对已区分的并集视为本身是已区分的并集。从逻辑上讲,现在判别式是两种并集类型(即所有判别式对的集合;在您的情况下为{["num","num"]["num","sum"]["sum","num"]["sum","sum"]}),但编译器未进行此类分析。我认为对于编译器来说,检查这些东西将太昂贵了,因为要检测何时使用了区分的联合,这样会花费一些时间,通常会被浪费掉。

您可以通过在一对有区别的联合中综合自己的有区别的联合,从而迫使编译器执行此操作,在该对中,您可以给出从区别产品的每个元素到新的被区别的显式映射。这是跳圈代码:

type DiscriminatedUnionPair<
  U, K extends keyof U,
  V, L extends keyof V,
  M extends Record<keyof M, readonly [U[K], V[L]]>> = {
    [P in keyof M]: {
      kind: P,
      first: Extract<U, Record<K, M[P][0]>>,
      second: Extract<V, Record<L, M[P][1]>>
    }
  }[keyof M];

function discriminatedUnionPair<U, K extends keyof U,
  V, L extends keyof V,
  M extends Record<keyof M, readonly [U[K], V[L]]>
>(
  firstUnion: U,
  firstDiscriminantKey: K,
  secondUnion: V,
  secondDiscriminantKey: L,
  disciminantMapping: M
): DiscriminatedUnionPair<U, K, V, L, M> {
  const p = (Object.keys(disciminantMapping) as Array<keyof M>).find(p =>
    disciminantMapping[p][0] === firstUnion[firstDiscriminantKey] &&
    disciminantMapping[p][1] === secondUnion[secondDiscriminantKey]
  );
  if (typeof p === "undefined") throw new Error("WHAT, MAPPING IS BAD");
  return { kind: p, first: firstUnion, second: secondUnion } as any;
}

该代码可以在某个地方的库中删除。然后,您可以像这样实现isEqual()而不会出现错误:

function isEqual(e1: Expr, e2: Expr): boolean {
  const m = {
    sumsum: ["sum", "sum"],
    sumnum: ["sum", "num"],
    numsum: ["num", "sum"],
    numnum: ["num", "num"]
  } as const;
  const e = discriminatedUnionPair(e1, "type", e2, "type", m)
  switch (e.kind) {
    case 'numsum': return false;
    case 'sumnum': return false;
    case 'numnum': return e.first.v === e.second.v;
    case 'sumsum': return isEqual(e.first.left, e.second.left) &&
      isEqual(e.first.right, e.second.right);
  }
}

重要的一行是对discriminatedUnionPair(e1, "type", e2, "type", m)的调用,该调用产生了类型为{kind: 'sumsum', first: Sum, second: Sum} | {kind: 'sumnum', first: Sum, second: Num} | {kind: 'numsum', first: Num, second: Sum} | {kind: 'numnum', first: Num, second: Num}的新元素,TypeScript 确实将识别为您所期望的歧视联合,并且代码按照您想要的方式运行:

const n1: Num = { type: "num", v: 1 }
const n2: Num = { type: "num", v: 2 }
const s1: Sum = { type: "sum", left: n1, right: n2 }
const s2: Sum = { type: "sum", left: n2, right: n1 }

console.log(isEqual(n1, n1)); // true
console.log(isEqual(n1, n2)); // false
console.log(isEqual(n1, s1)); // false
console.log(isEqual(s2, n2)); // false
console.log(isEqual(s1, s2)); // false
console.log(isEqual(s2, s2)); // true

这当然是不值得的,因为您要在运行时强制存在一个新的有区别的联合,该联合仅在编译时才有意义。并且您正在手动枚举所有可能的判别对,这些对可能很快就会变旧。但是我只是想展示什么是可能的。

‍‍

希望有所帮助;祝你好运!