Post

Typescript Pt. 6 - Generics

What are Generics?

Generics are a way to make components work with any data type and not restrict to one data type. For example, a function that returns the first element of an array:

1
2
3
4
const cars = ["ford", "toyota", "chevy"];
const house: Array<number> = [1, 2, 3]; // this is a generic with type annotation (number)

const car = cars[0];
  • Generics are features that allow us to define reusable blocks of code (functions) that can be used with different types.

Creating a Generic Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// the function below is not generic because it only works with numbers
const add = (a: number, b: number): number => {
  return a + b;
};

// the function below is not generic because it only works with strings
const join = (a: string, b: string): string => {
  return a + b;
};

// Generic function
// the function below is generic because it works with any type
const addGeneric = <T>(a: T, b: T): T => {
  return a + b;
};
// T is the generic type

Consider the following example:

1
2
3
function identity<T>(arg: T): T {
  return arg;
}
  • The <T> is a generic type. It tells TypeScript that this is a generic function.
  • The function above can be called with any type. For example:
1
2
3
identity<string>("myString"); // returns a string
identity<number>(5); // returns a number
identity<boolean>(true); // returns a boolean
  • This means that whatever type we pass in, the function will return the same type.
  • Type can be given an arbitrary name, but it’s common to use T as a generic type name.

In the next example, we will create a generic function that takes an array of any type and returns a random element from that array:

1
2
3
4
5
6
7
8
9
10
11
12
13
// const randomElement = <T>(arr: T[]): T => {
//   return arr[Math.floor(Math.random() * arr.length)];
// };
function randomElement<T>(arr: T[]): T {
  const randomIndex = Math.floor(Math.random() * arr.length);
  return arr[randomIndex];
}

const fruits = ["apple", "orange", "banana"];
const randomFruit = randomElement(fruits); // returns a random fruit

const numbers = [1, 2, 3];
const randomNumber = randomElement(numbers); // returns a random number

Inference with Generics

Sometimes, TypeScript can infer the type of the generic function based on the arguments passed to it. For example:

1
2
3
4
5
const identity = <T>(arg: T): T => {
  return arg;
};

const myIdentity = identity("myString"); // TypeScript infers that the type of myIdentity is string

Some other times, TypeScript cannot infer the type of the generic function based on the arguments passed to it. For example:

1
2
3
4
5
const identity = <T>(arg: T): T => {
  return arg;
};

const myIdentity = identity(5); // TypeScript cannot infer the type of myIdentity

In the example above, TypeScript cannot infer the type of myIdentity because the argument passed to the identity function is a number, and the generic type T is not specified.

Generic function with multiple types

  • Generic functions can have multiple types. For example:
1
2
3
4
5
const identity = <T, U>(arg1: T, arg2: U): T => {
  return arg1;
};

const myIdentity = identity("myString", 5); // TypeScript infers that the type of myIdentity is string
  • when passing multiple types to a generic function, the order of the types matters. For example:
1
2
3
4
5
const identity = <T, U>(arg1: T, arg2: U): T => {
  return arg1;
};

const myIdentity = identity(5, "myString"); // TypeScript infers that the type of myIdentity is number
  • The convention is to go according to the letters of the alphabet. Like in the example above, T comes before U, so the type of myIdentity is number. The next type would be V, then W, and so on.

Generic Constraints

  • Generic constraints allow us to limit the types that can be passed to a generic function.
  • For example, we can create a generic function that only accepts types that have a length property:
1
2
3
4
5
const identity = <T extends { length: number }>(arg: T): T => {
  return arg;
};

const myIdentity = identity("myString"); // TypeScript infers that the type of myIdentity is string
  • Another example is to create a generic function that only accepts objects as arguments:
1
2
3
4
5
const identity = <T extends object>(arg: T): T => {
  return arg;
};

const myIdentity = identity({ name: "John" }); // TypeScript infers that the type of myIdentity is object

Constraints with Interfaces

  • We can also use interfaces as constraints. For example:
1
2
3
4
5
6
7
8
9
interface HasLength {
  length: number;
}

const identity = <T extends HasLength>(arg: T): T => {
  return arg.length * 2;
};

const myIdentity = identity("myString"); // returns 16
  • In the example above, the generic function identity only accepts types that have a length property. The HasLength interface is used as a constraint.

Constraints with Classes

  • We can also use classes as constraints. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Car {
  drive() {
    console.log("driving...");
  }
}

class House {
  drive() {
    console.log("driving...");
  }
}

const identity = <T extends Car>(arg: T): T => {
  return arg;
};

const myIdentity = identity(new Car()); // returns a Car object
  • In the another example, we will have two infaces, one for a song with a title and artist property, and another for a movie with a title and director property:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Song {
  title: string;
  artist: string;
}

interface Movie {
  title: string;
  director: string;
  releaseYear: number;
}

class Playlist<T> {
  public queue: T[] = [];

  addToQueue(item: T) {
    this.queue.push(item);
  }

  getNextItem(): T {
    return this.queue.shift();
  }
}
  • The class in the above example is a generic class. It can be used with any type. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const playlist = new Playlist<Song>();
playlist.addToQueue({
  title: "Song Title",
  artist: "Artist Name",
});

const nextSong = playlist.getNextItem(); // returns a Song object

const moviePlaylist = new Playlist<Movie>();
moviePlaylist.addToQueue({
  title: "Movie Title",
  director: "Director Name",
  releaseYear: 2020,
});

const nextMovie = moviePlaylist.getNextItem(); // returns a Movie object
This post is licensed under CC BY 4.0 by the author.