Да, утверждение типа, вероятно, единственный способ получить то, что вы хотите, с минимальными усилиями, извините.
Я просто хочу добавить к этому вопросу, что проблема в том, что пара дискриминируемых объединений не рассматривается компилятором как сам дискриминируемый союз. По логике это так, где дискриминант теперь является произведением дискриминантов двух типов объединений (то есть набора всех пар дискриминантов; в вашем случае это будет {["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
Вероятно, это того не стоит, поскольку вы заставляете существовать новый дискриминируемый союз во время выполнения, который имеет значение только во время компиляции. И вы вручную перечисляете все возможные дискриминантные пары, которые могут быстро состариться. Но я просто хотел показать, что это возможно.
?????
Надеюсь, это поможет; удачи!