Generic interfaces and classes
Generics are not just for functions — interfaces, type aliases, and classes can be generic too.
This is how you build reusable data structures and containers that keep the type of what they hold:
a Box<T>, a Repository<T>, a Result<T>. This lesson is generic types, which are the backbone of
every typed library and much of your own reusable code.
Generic type aliases and interfaces
A type parameter on a type alias or interface makes the shape generic:
// a generic type alias — a box holding a value of type T
type Box<T> = {
value: T;
};
const stringBox: Box<string> = { value: "Pune" }; // Box holding a string
const numberBox: Box<number> = { value: 42 }; // Box holding a number
stringBox.value.toUpperCase(); // fine — value is string
numberBox.value.toFixed(); // fine — value is number
Box<T> is a family of types — Box<string>, Box<number>, Box<User> — one per type you fill in.
You have used generic types constantly: Array<T>, Promise<T>, Map<K, V>, and the Result<T> and
State<T> discriminated unions from the unions module. Anywhere a type "holds" or "produces" some other
type, it is a good candidate to be generic:
interface ApiResponse<T> {
data: T;
status: number;
timestamp: Date;
}
type Result<T, E = string> = // two type parameters, E defaulting to string
| { ok: true; value: T }
| { ok: false; error: E };
const userResponse: ApiResponse<{ name: string }> = {
data: { name: "Kavita" }, status: 200, timestamp: new Date(),
};
ApiResponse<T> is the same envelope (data, status, timestamp) for any payload type — write it
once, use it for every endpoint. This is exactly how you type an API client (the capstone): a generic
response wrapper that keeps each endpoint's specific data type.
Generic classes
A class can be generic, which is how you build a container with behaviour that preserves its element type:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
numbers.push("three"); // error: 'string' is not assignable to 'number'
const top = numbers.pop(); // number | undefined
Stack<T> holds Ts and its methods speak in T — push takes a T, pop returns T | undefined.
Create a Stack<number> and it is a stack of numbers: pushing a string is an error, and popping gives a
number. One class definition, a type-safe stack for any element type. This is how every typed
collection library (immutable data structures, queues, trees) is built, and how you would build your
own.
Note the type parameter is fixed per instance: new Stack<number>() is forever a stack of numbers.
The class's methods can still have their own generic parameters (a map<R> that transforms T to
R), as the generic-functions lesson showed.
A generic repository — the everyday pattern
The most common generic class in real applications is a repository — a typed store for entities:
interface HasId {
id: string;
}
class Repository<T extends HasId> { // constrained: every T must have an id
private items = new Map<string, T>();
save(item: T): void {
this.items.set(item.id, item);
}
findById(id: string): T | undefined {
return this.items.get(id);
}
all(): T[] {
return [...this.items.values()];
}
}
interface User extends HasId { name: string; }
const users = new Repository<User>();
users.save({ id: "1", name: "Kavita" });
const user = users.findById("1"); // User | undefined — the full type, with name
Repository<T extends HasId> combines everything: a generic class, a constraint (T must have an
id), and methods that return the precise entity type. One Repository class serves users, orders,
products — every entity that has an id — each fully typed. This is the shape of a data layer in real
TypeScript backends, and building one is a rite of passage.
Map<K, V> and multiple type parameters
Generic types often have several type parameters, the canonical example being Map<K, V>:
const prices = new Map<string, number>(); // keys are strings, values are numbers
prices.set("chai", 15);
prices.set("chai", "free"); // error: 'string' is not assignable to 'number' (the VALUE type)
const p = prices.get("chai"); // number | undefined
Map<K, V> is generic in both its key type K and its value type V, and TypeScript checks both —
you cannot set a wrong-typed key or value, and get returns V | undefined. Building your own
two-type-parameter generic (a typed cache, a bidirectional map) follows the same pattern.
When to make a type generic
The judgement is the same as for functions: make a type generic when it holds or relates another
type that should be preserved. A Box, a Response, a Stack, a Repository, a Result — all
hold or produce some other type, and being generic keeps that type precise. Do not make a type
generic that does not actually vary — a User with fixed fields is not generic; a Container that
could hold anything is. And prefer the built-in generic types (Array, Map, Set, Promise,
Record) over hand-rolling your own when they fit — you rarely need to write your own Stack because
an array does the job, but you will write generic response wrappers, result types, and repositories.
Check your work
What a generic type alias/interface is. A shape with a type parameter — Box<T>, ApiResponse<T> —
a family of types, one per filled-in type.
Where you have used generic types. Array<T>, Promise<T>, Map<K,V>, and Result<T>/State<T>.
What a generic class is. A container with behaviour that preserves its element type — Stack<T>
holds Ts and its methods speak in T.
Whether a class's type parameter varies per instance. No — new Stack<number>() is fixed as a
stack of numbers; methods can have their own generic parameters, though.
The repository pattern. Repository<T extends HasId> — a generic, constrained class returning the
precise entity type; the shape of a typed data layer.
Map<K, V>. A generic type with two type parameters, both checked — keys and values typed
independently.
When to make a type generic. When it holds or relates another type that should be preserved; not when it does not actually vary; prefer built-in generics when they fit.
Practice
- Write a generic
Box<T>type alias and use it for a string and a number. Confirm.valuehas the right type in each. - Write a generic
ApiResponse<T>interface and type two different endpoint responses with it. - Write a generic
Stack<T>class. Create aStack<number>, push a number (fine) and a string (error), and confirmpop()returnsnumber | undefined. - Write a generic
Repository<T extends HasId>class. Store aUser, find one, and confirm the full type comes back. - Add a
map<R>method toBox<T>that returns aBox<R>, and use it to turn aBox<number>into aBox<string>. - Use
Map<string, number>and confirm both key and value types are checked andgetisV | undefined. - For three types in code you know, decide whether each should be generic and justify it.
Official documentation
- TypeScript — Generic Types — Generic interfaces and type aliases.
- TypeScript — Generic Classes — Classes with type parameters.
- TypeScript — Map and Set — The built-in generic collections.
Next: the built-in utility types — Partial, Pick, Omit and Record.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship