Discriminated Union
If you have a class with a literal member (the literal TypeScript supports at the moment are string literals) then you can use that property to discriminate between union members.
As an example consider the union of a Square
and Rectangle
, here we have a member kind
that exists on both union members and is of a particular literal type:
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
If you use a type guard style check (==
, ===
, !=
, !==
) or switch
on the discriminant property (here kind
) TypeScript will realize that it means that the object must of the type that has that literal and do a type narrowing for you :)
function area(s: Shape) {
if (s.kind === "square") {
// Now TypeScript *knows* that `s` must a square ;)
// So you can use its members safely :)
return s.size * s.size;
}
else {
// Wasn't a square? So TypeScript will figure out that it must be a Rectangle ;)
// So you can use its members safely :)
return s.width * s.height;
}
}
Exhaustive Checks
Quite commonly you want to make sure that all members of a union have some code(action) against them.
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
// Someone just added this new `Circle` Type
// We would like to let TypeScript give an error at any place that *needs* to cater for this
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
As an example of where stuff goes bad:
function area(s: Shape) {
if (s.kind === "square") {
return s.size * s.size;
}
else if (s.kind === "rectangle") {
return s.width * s.height;
}
// Would it be great if you could get TypeScript to give you an error?
}
You can do that by simply adding a fall through and making sure that the inferred type in that block is compatible with the never
type. For example:
function area(s: Shape) {
if (s.kind === "square") {
return s.size * s.size;
}
else if (s.kind === "rectangle") {
return s.width * s.height;
}
else {
// ERROR : `Circle` is not assignable to `never`
const _exhaustiveCheck: never = s;
}
}
Switch
TIP: of course you can also do it in a switch
statement:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius * s.radius;
default: const _exhaustiveCheck: never = s;
}
}
strictNullChecks
If using strictNullChecks and doing exhaustive checks you should return the _exhaustiveCheck
variable (of type never
) as well, otherwise TypeScirpt infers a possible return of undefined
. So:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius * s.radius;
default:
const _exhaustiveCheck: never = s;
return _exhaustiveCheck;
}
}