Discriminated unions in TypeScript
A discriminated union is a union type who could be any of a given set of types, but not eachother.
It's useful for a situation where you have several different, conflicting shapes that a single given value could be.
It means that you'll be able to get TypeScript compiler errors for non-exhaustive checks, rather than finding out in production.
The naive approach
Naively, it looks like:
type Rectangle = {
width: number;
height: number;
}
type Circle = {
radius: number;
}
type Shape = Rectangle | Circle;
However, you'll find this doesn't work quite like you'd expect.
const rectangle: Shape = {
width: 50,
height: 50,
};
const circle: Shape = {
radius: 50,
};
// This is technically a valid Shape!
const thing: Shape = {
radius: 50,
width: 50,
};
The thing
down there, with a radius
and a width
but no height is still a valid Shape
.
The documentation isn't clear on this, but I suspect that this is because:
thing
satisfiesCircle
, which satisfiesShape
thing
only contains known properties ofShape
Discriminating the types
Often this pattern is done to differentiate from a number of known interface responses. Maybe that interface is a library, API, whatever.
Like:
type SuccessResponse = {
result: "ok";
data: SuccessBody;
};
type ErrorResponse = {
result: "error";
error: Error;
};
type Response = SuccessResponse | ErrorResponse;
const doApiRequest = () => {
const response: Response = fetch(...);
if (response.result === error) {
// Here TS can type narrow to know this is an ErrorResponse
throw new Error(response.error);
}
// Here TS can type narrow to know this is a SuccessResponse
// ...
};
This works because the two types being discriminated have a literal result
which differentiates them.
Using never
to differentiate the types
But you can also do this using never
, if you need an input type, maybe for component props rather than one coming from an interface.
Like:
type Rectangle = {
width: number;
height: number;
radius?: never;
};
type Circle = {
radius: number;
width?: never;
height?: never;
};
type Shape = Rectangle | Circle;
This makes the thing
from earlier no longer allowed:
// Type '{ radius: number; width: number; }' is not assignable to type 'Shape'.
// Types of property 'width' are incompatible.
// Type 'number' is not assignable to type 'undefined'.(2322)
const thing: Shape = {
radius: 50,
width: 50,
};