Skip to content
Theory · Generics · lesson 7 of 10

Generic types

So far only functions had a type parameter. But a container in the port has its label holder from the start: containers are made from one design, and the cargo and the label are decided later. Types need the same.

Imagine that a program keeps values in a wrapper { value: … }: sometimes a number, sometimes a string. Without a type parameter you would write a separate type for each case: type NumberBox = { value: number }, type StringBox = { value: string } and so on. The shape is the same, only the type inside differs—the same problem as with the function first.

An alias with a type parameter

A generic type declares a type parameter in angle brackets after its name, as a function does:

TypeScript
type Box<T> = { value: T };

const age: Box<number> = { value: 30 };
const nick: Box<string> = { value: "ann" };
console.log(age.value + 1, nick.value); // 31 ann

Let's take the declaration apart:

  • type Box<T> declares a type alias with the parameter T;
  • { value: T } is the object shape, with T in place of the property's type;
  • Box<number> is the type with an argument: number is put in place of T, which gives the shape { value: number }.

For a function, T is inferred from the arguments of a call. A type has no call, so you write the type argument yourself: Box<number>, Box<string>. The program prints 31 ann: age.value has the type number, and you can add one to it.

A generic interface

An interface can have a type parameter too. Server responses are often described this way: the wrapper is the same, and the data inside differs:

TypeScript
interface User {
    id: number;
    nick: string;
}

interface ApiResponse<T> {
    ok: boolean;
    data: T;
}

const res: ApiResponse<User> = {
    ok: true,
    data: { id: 1, nick: "ann" },
};
console.log(res.data.nick); // ann

ApiResponse<User> is a response whose data field holds a User. The same interface describes a list as well: ApiResponse<User[]>.

Here the container picture ends. You cannot put a real container inside another, but a type argument can be another generic type: Box<Box<number>> is a box holding a box with a number.

Interviewers often give this task: describe a server response whose wrapper is shared by all requests while the data differs. The expected solution is a generic interface like ApiResponse<T>.

Array<T> is a generic type too

Now you can read Array<number> to the end. In TypeScript's standard library an array is described as interface Array<T>, and its methods speak of T:

  • push(...items: T[]): number takes elements of type T;
  • pop(): T | undefined returns an element of type T, or undefined if the array is empty;
  • length: number does not depend on T.

number[] is the short form of the same Array<number>. In it T is number, which is why [1, 2].pop() has the type number | undefined.

A default type

A type parameter can have a default value, written with =, like a function parameter:

TypeScript
type Field<T = string> = { label: string; value: T };

const city: Field = { label: "City", value: "Paris" };
const year: Field<number> = { label: "Year", value: 2026 };
console.log(city.value, year.value); // Paris 2026

Let's go line by line:

  1. <T = string> says: if no type argument is written, T is string.
  2. city has no argument, and the editor's hover shows the type Field<string>.
  3. year has an argument written, so the default is not used: the type is Field<number>.
  4. The program prints Paris 2026.

Common mistake

A generic type is used without an argument:

TypeScript
type Box<T> = { value: T };

const box: Box = { value: 1 }; // ✗
TEXT
error TS2314: Generic type 'Box' requires 1 type argument(s).

The message says: the generic type Box needs one type argument. It is like shipping a container with an empty label holder: nobody knows which cargo to check it against. TypeScript will not infer T from the value { value: 1 }, because a type has no call. Write the argument, Box<number>, or give the parameter a default: type Box<T = number>.

Key points

  • A generic type declares its parameter in angle brackets: type Box<T> = { value: T } or interface ApiResponse<T>.
  • For a type, T is not inferred; you write the argument yourself: Box<number>.
  • Array<T> is a generic interface of the standard library, and number[] is the short form of Array<number>.
  • A default <T = string> lets you omit the argument: Field means Field<string>.