Skip to content

Generics

If you consider types like variables, then generics could be considered as functions. They allow you to create types that are parameterized by other types.

Generics are a powerful feature of TypeScript that allows you to create reusable types that can work in various contexts.

Generics in function signatures

A common use case for generics is to create functions and indicate a relation between the arguments and the return type.

Let's take a simple example of a function that returns the first element of an array:

typescript
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

To declare a generic type, you use the angle brackets syntax <T>. Here T is a type variable that can be used in the function signature. It is used in the argument type annotation for arr as T[], meaning the function expects an array of a generic type, then it is used in return type definition as T | undefined, meaning the function will return either an element of that generic type T or undefined if the array is empty.

Any number of generic types can be used. For example, a function that randomly returns either the first argument or the second:

typescript
function randomElement<A, B>(a: A, b: B): A | B {
  return Math.random() < 0.5 ? a : b;
}

By convention, generic types are most often named with a single uppercase letter, but you can use any name you want.

Generics in type definitions

Generics can also be used in type definitions. For example, a type that represents a pair of values of different types:

typescript
type Pair<First, Second> = [First, Second];

or a generic collection type:

typescript
type Stack<T> = {
  length: number;
  push: (item: T) => void;
  pop: () => T | undefined;
};

In that case, you will have to specify the type T when you use the type Collection:

typescript
const logs: Stack<string> = ["hello world"];

Generics constraints with extends

By default, a generic type can work with any type. However, you can restrict the types that can be used with a generic type by using the extends keyword.

We have already seen this keyword extends in the previous chapter with conditional types. U extends T is used to indicate than the type U is T or a subtype of T, i.e. U is a valid T but is possibly an even more narrowed type. For example, string extends string is valid but also string extends string | number, or "literal" extends string.

typescript
type T = "hello" extends string ? true : false; // T = true

In the context of generics, extends will give more control to the developer and raise errors if the type does not match the constraint. Here are a few examples:

typescript
type HasLength = { length: number };

function longest<A extends HasLength, B extends HasLength>(a: A, b: B): A | B {
  return a.length >= b.length ? a : b;
}

longest("string", ["a", "r", "r", "a", "y"]);

longest([1, 2, 3], new Set([4, 5, 6]));
// Error: Property 'length' is missing in type 'Set<number>'
// but required in type 'HasLength'
typescript
function getProperty<Obj, Key extends keyof Obj>(obj: Obj, key: Key): Obj[key] {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m");
// Error: Argument of type '"m"' is not assignable to parameter
// of type '"a" | "b" | "c" | "d"'.

Exercise 1

Follow instructions in the following playground for a practice exercise on generics.

Generic types defaults

A generic type can have a default value, which will be used if the type is not specified when using the type. Example:

typescript
type Stack<T = string> = {
  length: number;
  push: (item: T) => void;
  pop: () => T | undefined;
};

const stack1: Stack = [];
stack1.push("hello world");
stack1.push(42); // Error: Argument of type '42' is not assignable
// to parameter of type 'string'

function createStack<T = string>(initial: T[] = []): Stack<T>;

const stack = createStack(); // stack is of type Stack<string> and is empty
const numberStack = createStack<number>([1, 2]); // Stack<number> containing [1, 2]

The infer keyword

The infer keyword is used in conditional types to infer a type from another type. It is also used in generics to infer a type from a value. For example, here is how you can infer the type of an array element:

typescript
type ElementType<A> = A extends (infer E)[] ? E : never;

type T = ElementType<string[]>; // T is string
type U = ElementType<number[]>; // U is number

Here is how you can infer the return type of a function:

typescript
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type T = ReturnType<typeof Math.max>; // T is number

Or the type of the first argument of a function:

typescript
type FirstArg<F> = F extends (first: infer A, ...args: any[]) => any ? A : never;

type T = FirstArg<typeof String.fromCharCode>; // T === number

In this example, FirstArgument is a generic type that takes a function type F as a parameter. It checks if F is a function that takes at least one argument, and if so, the infer keyword is used to extract the type of the first argument from the function type.

Note the never keywords in the second part of the conditional. It is a special type that matches no value at all. It's basically an empty set used to raise an error if the argument passed to FirstArgument is not a function with one argument at least.

Exercise 2

Follow instructions in the following playground

TRIVIA

Generics allow TypeScript to be a turing-complete type system, meaning that it can express any computation that can be computed. This has been brillantly demonstrated recently by someone who managed to run the video game Doom in the TypeScript type system.