I'm having trouble understanding discriminated unions in typescript in the context of an array. Using the example from the documentation, I would expect the below to be perfectly valid.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
const getShapes = (): Shape[] => {
const data = [
{ kind: "circle", radius: 1 },
{ kind: "square", sideLength: 3}
]
return data;
}
My understanding is this is saying "a Shape must be either a Circle or a Square".
Instead it gives this error:
Type '({ kind: string; radius: number; sideLength?: undefined; } | { kind: string; sideLength: number; radius?: undefined; })[]' is not assignable to type 'Shape[]'. Type '{ kind: string; radius: number; sideLength?: undefined; } | { kind: string; sideLength: number; radius?: undefined; }' is not assignable to type 'Shape'. Type '{ kind: string; radius: number; sideLength?: undefined; }' is not assignable to type 'Shape'. Type '{ kind: string; radius: number; sideLength?: undefined; }' is not assignable to type 'Square'. Types of property 'kind' are incompatible. Type 'string' is not assignable to type '"square"'.
I'm specifically confused about seeing kind: string
as how typescript interpreted that, when it matches the literal. Based on how I'm reading the docs, this seems like a perfectly valid use for a union
When presented with a variable declaration const data = ⋯
with no type annotation, TypeScript infers the type of the variable from the initializer, based on heuristic inference rules that work well in a wide range of scenarios. It does not currently have the ability to defer that and "look ahead" to see how the variable is used later. Given
const data = [
{ kind: "circle", radius: 1 },
{ kind: "square", sideLength: 3 }
];
the compiler infers
const data: ({
kind: string;
radius: number;
sideLength?: undefined;
} | {
kind: string;
sideLength: number;
radius?: undefined;
})[]
because that's what inference rules say to do. In particular, string literal properties get widened to string
, since it's very common for people to modify the values of properties. The compiler does not see that you intend to return data
as Shape[]
, so it doesn't know that you intend for the kind
property to have string literal types. By the time you return data
is encountered, it is too late. The type of data
is already set.
If you want to fix this you should either annotate data
like const data: Shape[] = ⋯
, or you could use the satisfies
operator on the initializer to give the compiler the context it is missing:
const getShapes = (): Shape[] => {
const data = [
{ kind: "circle", radius: 1 },
{ kind: "square", sideLength: 3 }
] satisfies Shape[]
return data;
}