TypeScript: Generics

 by Robin Wieruch
 - Edit this Post

Generics in TypeScript are not easy to understand when just starting out with TS. Personally I had my struggles with them in the beginning, however, once you get how they are used, they make you a more complete TypeScript developer.

In this TypeScript tutorial, you will learn how to use generics in TypeScript. We will start with defining a JavaScript arrow expression (also called arrow function) which takes an object (here: dog) and returns its age property:

const getAge = (dog) => {
return dog.age;
};

Then we will call this function with an object which has this required property:

const trixi = {
name: 'Trixi',
age: 7,
};
console.log(getAge(trixi));
// 7

Now if we'd want to define this code in TypeScript, it would change the following way:

type Dog = {
name: string;
age: number;
};
const getAge = (dog: Dog) => {
return dog.age;
};
const trixi: Dog = {
name: 'Trixi',
age: 7,
};
console.log(getAge(trixi));
// 7

However, the function is specific to one TypeScript type (here: Dog) now. If we would be using a value of a different type as argument (e.g. Person), there would be a TypeScript error, because both types differ in their structure:

type Person = {
firstName: string;
lastName: string;
age: number;
};
const robin: Person = {
firstName: 'Robin',
lastName: 'Wieruch',
age: 7,
};
console.log(getAge(robin));
// Argument of type 'Person' is not assignable to parameter of type 'Dog'.
// Property 'name' is missing in type 'Person' but required in type 'Dog'.

The arrow function expects an argument of type Dog, as it's defined in the function signature, but in the previous example it received an argument of type Person which has different properties (even though both share the age property):

const getAge = (dog: Dog) => {
return dog.age;
};

In addition to giving the parameter a more abstract yet , one solution would be using a TypeScript union type:

const getAge = (mammal: Dog | Person) => {
return mammal.age;
};

And this solution would be alright for most TypeScript projects. However, once a project grows in size (vertically and horizontally), you will most certainly hit the need for TypeScript generics, because the function should accept any generic (you can also read: abstract) type which still fulfils certain requirements (here: having a age property).

Let's enter TypeScript generics ...

Generics in TypeScript

Once a project grows horizontally in size (e.g. more domains in a project), an abstract function like getAge may receive more than two types (here: Dog and Person) as arguments. In conclusion one would have to scale the union type horizontally too, which is tiresome (but still working) and error prone.

type Mammal = Person | Dog | Cat | Horse;

In the orthogonal direction, once a project grows vertically in size, functions that are getting more reusable and therefore abstract (like getAge) should rather deal with generic types instead of domain specific types (e.g. Dog, Person).

Popular Use Case: Most often you will see this in third-party libraries which do not know about the domain of your project (e.g. dog, person), but need to anticipate any type which fulfils certain requirements (e.g. required age property). Here third-party libraries cannot use union types anymore as an escape hatch, because they are not in the hands of the developer anymore who is working on the actual project.

Continue Reading:

In conclusion, if the getAge function should handle any entity with an age property, it must be generic (read: abstract). Therefore we need to use some kind of placeholder for using a generic type which is most often implemented as T:

type Mammal = {
age: number;
};
const getAge = <T extends Mammal>(mammal: T) => {
return mammal.age;
};

Whereas the T extends Mammal stands for any type which has an age property. While using the following works:

type Person = {
firstName: string;
lastName: string;
age: number;
};
const robin: Person = {
firstName: 'Robin',
lastName: 'Wieruch',
age: 7,
};
console.log(getAge(robin));

You have successfully used a TypeScript generic now. The abstract getAge() function takes as argument any object which has an age property. Neglecting the age property would give us a TypeScript error:

type Mammal = {
age: number;
};
const getAge = <T extends Mammal>(mammal: T) => {
return mammal.age;
};
type Person = {
firstName: string;
lastName: string;
age?: number;
};
const robin: Person = {
firstName: 'Robin',
lastName: 'Wieruch',
// age: 7,
};
console.log(getAge(robin));
// Argument of type 'Person' is not assignable to parameter of type 'Mammal'.
// Types of property 'age' are incompatible.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'

Generics are heavily used in third-party libraries. If a third-party library implements generics properly, you don't have to think much about them when using these abstract libraries in your domain specific TypeScript application.

Keep reading about 

Type Guards in TypeScript are needed whenever you want to allow only a certain type for a routine in TypeScript. In this TypeScript tutorial, you will learn how to check for user defined types by…

TypeScript is getting more popular these days for frontend and backend applications. Here you will learn how to set up TypeScript in Node.js for a backend project. The previous tutorial already…

The Road to React

Learn React by building real world applications. No setup configuration. No tooling. Plain React in 200+ pages of learning material. Learn React like 50.000+ readers.

Get it on Amazon.