When TypeScript infers types for values, it utilizes heuristic rules to ensure desirable behavior across various use cases. However, there are instances where these heuristics fall short of expectations.
One such rule involves unions of object literals being inferred with optional `undefined` properties from other members of the union. This transformation turns complex unions into discriminated unions, making them more manageable. Yet, in certain situations, this may lead to undesired type outcomes.
If TypeScript's type inference fails to align with your expectations, it is advisable to manually specify the expected type. By providing an annotation, you can guide the compiler and avoid unexpected inference results:
type DiscU = { type: "Quz"; str: "string"; } | { type: "Bar"; 1: "value"; };
function fooAnnotate(num: number): DiscU {
switch (num) {
case 0: return { type: "Quz", str: 'string', };
case 1: return { type: "Bar", 1: 'value' };
default: throw new Error("Unknown discriminant: " + num);
}
}
In situations where manual type specification is not permitted, alternate approaches must be considered.
To circumvent pre-computation when dealing with object literals, a common workaround involves assigning the literal to an intermediate variable before building the union:
function foo(num: number) {
const case0 = { type: "Quz", str: 'string' } as const;
const case1 = { type: "Bar", 1: 'value' } as const;
switch (num) {
case 0: return case0;
case 1: return case1;
default: throw new Error("Unknown discriminant" + num);
}
}
This method ensures the desired type output without engaging in precomputation.
An alternative approach to avoid precomputation is by using immediately-executed functions within the `switch` statement:
function foo(num: number) {
switch (num) {
case 0: return (() => ({ type: "Quz", str: 'string' } as const))();
case 1: return (() => ({ type: "Bar", 1: 'value' } as const))();
default: throw new Error("Unknown discriminant" + num);
}
}
The immediate function effectively manages to prevent unwanted properties while adequately meeting requirements.
If the `readonly` properties remain a concern, modifying the immediately-executed function to a stand-alone function returning a non-`readonly` variant could address that issue:
function foo(num: number) {
const mutable = <T extends object>(o: T): { -readonly [K in keyof T]: T[K] } => o;
switch (num) {
case 0: return mutable({ type: "Quz", str: 'string' } as const);
case 1: return mutable({ type: "Bar", 1: 'value' } as const);
default: throw new Error("Unknown discriminant" + num);
}
}
By utilizing this modified function approach, the exact desired type can be achieved without extensive manual intervention or precomputation.
Explore the Playground Link for this code snippet!