我有两节课:
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
函数中e2
和isEqual
的类型(在初始检查之后)必须相等,或者至少具有没有任何显式强制转换的常见超类型?
答案 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
这当然是不值得的,因为您要在运行时强制存在一个新的有区别的联合,该联合仅在编译时才有意义。并且您正在手动枚举所有可能的判别对,这些对可能很快就会变旧。但是我只是想展示什么是可能的。
希望有所帮助;祝你好运!