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
|