Feb 11th, 2022 - written by Kimserey with .
Last week we talked about type predicate allowing us to narrow down based a variable based on a predicate function. In today’s post, we’ll continue to explore some of the narrowing functionalities of Typescript by looking at discriminated union narrowing and exhaustiveness checking.
When we have a discriminated union with multiple interfaces:
1
2
3
4
5
6
7
8
9
10
11
12
interface Apartment {
type: "apartment";
address: string;
level: number;
}
interface House {
type: "house";
address: string;
}
type Home = Apartment | House;
we can make sure that the interfaces have a single property differentiating them between each other within the union, here we use type
which would be of type "apartment" | "house"
.
Using type
, we ca then narrow the types with a control flow statement like if/else
or switch/case
:
1
2
3
4
5
6
7
8
9
10
function getFullAddress(home: Home) {
switch (home.type) {
case "apartment":
// home is of type Apartment
return `${home.address}, level ${home.level}`;
case "house":
// home is of type House
return home.address;
}
}
Within the case statements, the compiler will know that the value of home is of a specific type rather than being the union.
If we add a default
case to the switch
, we will see that home
is of type never
. The never
type is a special type which can be assigned to any type - but no type can be assigned to never
, except never
itself.
Let’s demonstrate that by adding a default
case:
1
2
3
4
5
6
7
8
9
10
11
function getFullAddress(home: Home) {
switch (home.type) {
case "apartment":
return `${home.address}, level ${home.level}`;
case "house":
return home.address;
default:
const _exhaustiveCheck: never = home;
return _exhaustiveCheck;
}
}
Because there is no other type
than "apartment"
and "home"
, the type of home
is narrowed down to never
. And because home
is narrowed to never
, we are able to assign it to the variable _exhaustiveCheck: never
.
As mentioned earlier, only never
can be assigned to never
, any other type cannot be assigned to never
. This rule allows us to implement an exhaustive check as if we add a new type to the union, the compiler will complain that type x is not assignable to never
.
To illustrate this, we add a new type to our union:
1
2
3
4
5
6
interface Boat {
type: "boat";
portAddress: string;
}
type Home = Apartment | House | Boat;
By adding Boat
, the compiler will now highlight _exhaustiveCheck
and complain with Type 'Boat' is not assignable to type 'never'
.
Similarly the exauhstive check is also useful when the switch
is in the form of if/else
:
1
2
3
4
5
6
7
8
function getFullAddress(home: Home) {
if (home.type === "apartment") return `${home.address}, level ${home.level}`;
if (home.type === "house") return home.address;
const _exhaustiveCheck: never = home;
return _exhaustiveCheck;
}
and lastly we could also create a function which we can use instead of assigning a variable:
1
function exhaustiveCheck(_v: never) {}
which we can use:
1
2
3
4
5
6
7
8
9
10
function getFullAddress(home: Home) {
switch (home.type) {
case "apartment":
return `${home.address}, level ${home.level}`;
case "house":
return home.address;
default:
exhaustiveCheck(home);
}
}
If we miss a case, we will get the following compiler error on the argument passed to exhaustiveCheck
: Argument of type 'Boat' is not assignable to parameter of type 'never'
. And that concludes today’s post!
Today we looked at another type of narrowing with discriminated union. We saw how we could guide the compiler by using a switch case
on a common attribute between the interfaces. We then moved on to look at how we could implement an exhaustive check of all the cases using the special never
type. I hope you liked this post and I’ll see you on the next one!