Type Narrowing in TypeScript
TypeScript has a feature called type narrowing. It allows you to narrow down the type of a variable based on a condition.
TypeOf Guard
Consider the following example:
1
2
3
4
5
6
| const add = (a: number | string, b: number | string): number | string => {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
return `${a}${b}`;
};
|
- The function above takes two parameters,
a
and b
, which can be either a number or a string. - The function returns a number if both parameters are numbers, otherwise it returns a string.
- The
typeof
operator is used to check the type of a variable. - The
typeof
operator returns a string that represents the type of the variable. - The
typeof
operator can be used with any;- type, including primitive types, object types, and function types.
- variable, including variables that are not declared.
- expression, including expressions that are not valid.
- value, including values that are not valid.
- type, including types that are not valid.
- variable, including variables that are not valid.
- expression, including expressions that are not valid.
- value, including values that are not valid.
- type, including types that are not valid.
- variable, including variables that are not valid.
- expression, including expressions that are not valid.
- value, including values that are not valid.
Truthy Guard
- This involves checking if a variable is truthy or falsy. For example:
1
2
3
4
5
6
| const add = (a: number | string, b: number | string): number | string => {
if (a && b && typeof a === "number" && typeof b === "number") {
return a + b;
}
return `${a}${b}`;
};
|
- The function above takes two parameters,
a
and b
, which can be either a number or a string. - The function returns a number if both parameters are numbers, otherwise it returns a string.
Equality Guard
- This involves checking if a variable is equal to a value. For example:
1
2
3
4
5
6
| const add = (a: number | string, b: number | string): number | string => {
if (a === 0 && b === 0) {
return a + b;
}
return `${a}${b}`;
};
|
- The function above takes two parameters,
a
and b
, which can be either a number or a string. - The function returns a number if both parameters are numbers, otherwise it returns a string.
In Guard
- This involves checking if a variable is in a list of values. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| interface Movie {
title: string;
year: number;
duration: number;
}
interface TVShow {
title: string;
episodes: number;
episodeLength: number;
}
function getRuntime(media: Movie | TVShow): number {
if ("duration" in media) {
return media.duration;
}
return media.episodes * media.episodeLength;
}
const movie: Movie = {
title: "The Shawshank Redemption",
year: 1994,
duration: 142,
};
const tvShow: TVShow = {
title: "Game of Thrones",
episodes: 73,
episodeLength: 60,
};
console.log(getRuntime(movie)); // 142
console.log(getRuntime(tvShow)); // 4380
|
- In the example above, the
getRuntime
function takes a Movie
or TVShow
as a parameter. - If the parameter is a
Movie
, the function returns the duration
property. - If the parameter is a
TVShow
, the function returns the episodes
property multiplied by the episodeLength
property.
InstanceOf Narrowing
instanceof
is a JavaScript operator that checks if one thing is an instance of another thing. For example if an object is an instance of a class.- In TypeScript,
instanceof
can be used to narrow down the type of a variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| class Movie {
title: string;
year: number;
duration: number;
constructor(title: string, year: number, duration: number) {
this.title = title;
this.year = year;
this.duration = duration;
}
}
class TVShow {
title: string;
episodes: number;
episodeLength: number;
constructor(title: string, episodes: number, episodeLength: number) {
this.title = title;
this.episodes = episodes;
this.episodeLength = episodeLength;
}
}
function getRuntime(media: Movie | TVShow): number {
if (media instanceof Movie) {
return media.duration;
}
return media.episodes * media.episodeLength;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function printFullDate(date: Date | string): void {
if (date instanceof Date) {
//console.log(date.toDateString());
console.log(date.toUTCString());
} else {
//console.log(new Date(date).toDateString());
console.log(new Date(date).toUTCString());
}
}
//printFullDate(new Date()); // Mon Oct 04 2021
//printFullDate("2021-10-04"); // Mon Oct 04 2021
printFullDate(new Date()); // Mon, 04 Oct 2021 00:00:00 GMT
printFullDate("2021-10-04"); // Mon, 04 Oct 2021 00:00:00 GMT
|
- In the example above, the
printFullDate
function takes a Date
or string
as a parameter. - If the parameter is a
Date
, the function prints the date in the format Mon Oct 04 2021
. - If the parameter is a
string
, the function prints the date in the format Mon Oct 04 2021
.
Type Predicates
- A type predicate is a function that returns a boolean value.
- A type predicate is used to narrow down the type of a variable.
- It gives us more direct control over how types change throughout our code.
1
2
3
4
5
6
7
8
9
10
| function isMovie(media: Movie | TVShow): media is Movie {
return (media as Movie).duration !== undefined;
}
function getRuntime(media: Movie | TVShow): number {
if (isMovie(media)) {
return media.duration;
}
return media.episodes * media.episodeLength;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| type Fish = { swim: () => void }; // Fish type has a swim method that returns void (nothing)
type Bird = { fly: () => void }; // Bird type has a fly method that returns void (nothing)
declare function getSmallPet(): Fish | Bird;
// the following function is a type predicate (especially the 'pet is Fish' part)
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
//
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
|
- In the example above, the
isFish
function takes a Fish
or Bird
as a parameter. - If the parameter is a
Fish
, the function returns true
. - If the parameter is a
Bird
, the function returns false
.
Discriminated Unions
- Discriminated unions are a pattern that allows us to narrow down the type of a variable.
- It is a combination of the
in
operator and the instanceof
operator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| interface Movie {
type: "movie";
title: string;
year: number;
duration: number;
}
interface TVShow {
type: "tvshow";
title: string;
episodes: number;
episodeLength: number;
}
function getRuntime(media: Movie | TVShow): number {
switch (media.type) {
case "movie":
return media.duration;
case "tvshow":
return media.episodes * media.episodeLength;
}
}
const movie: Movie = {
type: "movie",
title: "Tenet",
year: 2020,
duration: 142,
};
const tvShow: TVShow = {
type: "tvshow",
title: "Game of Thrones",
episodes: 73,
episodeLength: 60,
};
console.log(getRuntime(movie)); // 142
console.log(getRuntime(tvShow)); // 4380
|
- In the example above, the
getRuntime
function takes a Movie
or TVShow
as a parameter. - The
type
property is used to narrow down the type of the media
parameter. - If the
type
property is movie
, the function returns the duration
property. - If the
type
property is tvshow
, the function returns the episodes
property multiplied by the episodeLength
property. - The
type
property is called a discriminant.
Exhaustiveness checking with Never
- Exhaustiveness checking is a feature that allows us to check if we have covered all possible cases in a switch statement.
- If we have not covered all possible cases, TypeScript will give us an error.
- The
never
type is used to tell TypeScript that a function will never return. - The
never
type is used to tell TypeScript that a variable will never have a value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| interface Movie {
type: "movie";
title: string;
year: number;
duration: number;
}
interface TVShow {
type: "tvshow";
title: string;
episodes: number;
episodeLength: number;
}
function getRuntime(media: Movie | TVShow): number {
switch (media.type) {
case "movie":
return media.duration;
case "tvshow":
return media.episodes * media.episodeLength;
default:
const exhaustiveCheck: never = media;
return exhaustiveCheck;
}
}
const movie: Movie = {
type: "movie",
title: "Tenet",
year: 2020,
duration: 142,
};
const tvShow: TVShow = {
type: "tvshow",
title: "Game of Thrones",
episodes: 73,
episodeLength: 60,
};
console.log(getRuntime(movie)); // 142
console.log(getRuntime(tvShow)); // 4380
|
- In the example above, the
getRuntime
function takes a Movie
or TVShow
as a parameter. - The
type
property is used to narrow down the type of the media
parameter. - If the
type
property is movie
, the function returns the duration
property. - If the
type
property is tvshow
, the function returns the episodes
property multiplied by the episodeLength
property. - The
default
case is used to tell TypeScript that we have covered all possible cases. - The
default
case is used to tell TypeScript that the media
parameter will never have a value other than movie
or tvshow
.