The Typescript compiler does not keep track of dependencies between different variables in a way that allows it to "remember" the equality of a.type
and b.type
. Despite being requested as a feature, the Typescript team, as evidenced by this issue, currently do not see it as a priority to implement.
If I were to refactor the code, I would simplify it using a switch statement like this:
switch (a.type) {
case "A": {
return b.type === "A" && a.propsOfA === b.propsOfA;
}
case "B": {
return b.type === "B" && a.propsOfB === b.propsOfB;
}
case "C": {
return b.type === "C" && a.propsOfC === b.propsOfC;
}
}
By condensing the checks down to three from nine, the code becomes more concise. Another approach is to redefine your types as follows, creating a safer version that disallows certain invalid object structures:
interface A {
type: "A";
propsOfA: string;
propsOfB?: never;
propsOfC?: never;
}
interface B {
type: "B";
propsOfA?: never;
propsOfB: number;
propsOfC?: never;
}
interface C {
type: "C";
propsOfA?: never;
propsOfB?: never;
propsOfC: boolean;
}
With these refined types, your original code will compile without errors and provide additional safety checks. If manually writing out these types seems daunting, you can generate them programmatically using the following utilities:
/**
* Creates a discriminated union type with enforced property requirements.
*/
type SafeUnion<T> = Simplify<T> extends infer _T ? _T extends unknown ? Replace<{[K in AnyKeyOf<T>]?: never}, _T> : never : never
/**
* Simplifies a given type for improved readability.
*/
type Simplify<T> = T extends unknown ? {[K in keyof T]: T[K]} : never
/**
* Combines two types similar to an intersection, but assigns shared properties according to the second type.
*/
type Replace<S, T> = Simplify<T & Omit<S, keyof T>>
/**
* Retrieves all keys present in any member of a union type.
*/
type AnyKeyOf<T> = T extends unknown ? keyof T : never
After defining the above utilities, your code implementation can be updated to:
type ABC = SafeUnion<A | B | C>
function isEqual(a: ABC, b: ABC) {
// ...
}
Playground Link