Post

Typescript Pt. 9 - Type Narrowing

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;
}
  • 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.

  • In the next example, we will use instanceof to narrow down the type of a variable.
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;
}
  • In the example above, the isMovie function takes a Movie or TVShow as a parameter.
  • If the parameter is a Movie, the function returns true.
  • If the parameter is a TVShow, the function returns false.
  • The isMovie function is used to narrow down the type of the media parameter in the getRuntime function.

  • Another example:
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.
This post is licensed under CC BY 4.0 by the author.