Skip to main content

The best code is code you write once. Not code you copy-paste. Not code you rewrite for every new type. Code that adapts to whatever type you throw at it while staying perfectly type-safe.

That's what generics do. A generic function is a function that writes itself for each type you call it with. You define the pattern once. TypeScript generates the specific version for each type.

This chapter is the gateway to everything that makes TypeScript powerful. Generics are the foundation of the advanced type system. Every utility type, every conditional type, every mapped type — they all build on generics. Master this chapter, and the rest of the book unlocks.

The Problem Generics Solve​

You've written this function a hundred times:

function firstElement(arr: string[]): string | undefined {
return arr[0];
}

It works for strings. But what about numbers?

function firstNumberElement(arr: number[]): number | undefined {
return arr[0];
}

And users?

function firstUserElement(arr: User[]): User | undefined {
return arr[0];
}

This is absurd. The logic is identical. Only the types differ. You're writing the same function over and over, violating the most fundamental principle of programming: don't repeat yourself.

Generics solve this:

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

const str = firstElement(["a", "b", "c"]); // Type: string | undefined
const num = firstElement([1, 2, 3]); // Type: number | undefined
const user = firstElement([alice, bob]); // Type: User | undefined

<T> is a type parameter. It's a placeholder for a type that will be filled in when the function is called. TypeScript infers T from the argument — you pass string[], T becomes string. You pass User[], T becomes User.

One function. Any type. Perfect type safety.

Type Parameter Inference​

TypeScript infers type parameters from the arguments you pass:

function identity<T>(value: T): T {
return value;
}

const x = identity("hello"); // T inferred as string
const y = identity(42); // T inferred as number
const z = identity({ name: "Alice" }); // T inferred as { name: string }

You CAN specify type parameters explicitly, but you rarely need to:

const x = identity<string>("hello"); // Explicit T = string
const y = identity<number>(42); // Explicit T = number

Explicit type parameters are useful when TypeScript can't infer correctly, or when you want to be explicit about intent.

Multiple Type Parameters​

Functions can have multiple type parameters:

function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}

const p1 = pair("hello", 42); // Type: [string, number]
const p2 = pair(true, { name: "Alice" }); // Type: [boolean, { name: string }]

Each type parameter is inferred independently from its corresponding argument.

Generic Constraints​

Sometimes you don't want to accept ANY type. You want to accept types that have certain properties:

function getLength<T extends { length: number }>(value: T): number {
return value.length;
}

getLength("hello"); // OK — string has length
getLength([1, 2, 3]); // OK — array has length
getLength({ length: 10 }); // OK — object has length
getLength(42); // Error: number doesn't have length

T extends { length: number } is a constraint. It says "T can be any type, as long as it has a length property of type number." TypeScript enforces this at the call site — you can't pass a number because number doesn't have length.

Constraints with Type Parameters​

You can constrain one type parameter by another:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };
const name = getProperty(user, "name"); // Type: string
const age = getProperty(user, "age"); // Type: number
const invalid = getProperty(user, "invalid"); // Error: 'invalid' is not a key of User

K extends keyof T means "K must be a key of T." TypeScript enforces this — you can only pass valid property names. And the return type T[K] is the type of that property — string for "name", number for "age".

This is one of the most useful generic patterns. It gives you type-safe property access without string literals that could be wrong.

Generic Interfaces and Type Aliases​

Generics aren't just for functions. Interfaces and type aliases can be generic too:

interface ApiResponse<T> {
data: T;
status: number;
message: string;
}

type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;
type StringResponse = ApiResponse<string>;

function handleResponse<T>(response: ApiResponse<T>): T {
return response.data;
}

Generic types are the foundation of reusable data structures:

interface Stack<T> {
push(item: T): void;
pop(): T | undefined;
peek(): T | undefined;
size: number;
}

type Dictionary<K extends string, V> = {
[key in K]: V;
};

type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };

Notice Result<T, E = Error> — type parameters can have defaults. If you don't specify E, it defaults to Error:

type StringResult = Result<string>; // E defaults to Error
type CustomResult = Result<string, ValidationError>; // E is ValidationError

Generic Classes​

Classes can be generic too:

class DataStore<T> {
private items: T[] = [];

add(item: T): void {
this.items.push(item);
}

getAll(): readonly T[] {
return this.items;
}

find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
}

const userStore = new DataStore<User>();
userStore.add(alice);
userStore.add(bob);
const users = userStore.getAll(); // Type: readonly User[]

Generic classes are the backbone of collections, repositories, and any data structure that works with multiple types.

The keyof Operator​

keyof extracts the keys of a type as a union:

interface User {
name: string;
age: number;
email: string;
}

type UserKeys = keyof User; // "name" | "age" | "email"

keyof is essential for writing generic functions that work with object properties:

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}

const users: User[] = [
{ name: "Alice", age: 30, email: "alice@example.com" },
{ name: "Bob", age: 25, email: "bob@example.com" },
];

const names = pluck(users, "name"); // Type: string[]
const ages = pluck(users, "age"); // Type: number[]

The typeof Operator (Type Context)​

In type position, typeof extracts the type of a JavaScript value:

const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
};

type Config = typeof config;
// { apiUrl: string; timeout: number; retries: number; }

This is different from JavaScript's typeof (which returns a string at runtime). In TypeScript's type system, typeof extracts the compile-time type of a value.

This is incredibly useful for deriving types from existing values:

const routes = {
home: "/",
about: "/about",
user: "/user/:id",
} as const;

type Route = typeof routes[keyof typeof routes];
// "/" | "/about" | "/user/:id"

Generic Defaults and Inference Precedence​

Type parameters can have defaults, and TypeScript infers them in a specific order:

function createMap<K extends string, V = string>(): Map<K, V> {
return new Map<K, V>();
}

const stringMap = createMap<"key1" | "key2">();
// K = "key1" | "key2", V = string (default)

const numberMap = createMap<"key1" | "key2", number>();
// K = "key1" | "key2", V = number (explicit)

Defaults are filled in left to right. You can't specify a later type parameter without specifying the earlier ones.

Real-World Generic Patterns​

Pattern 1: The Type-Safe Event Emitter​

type EventMap = {
userCreated: { name: string; email: string };
userDeleted: { id: number };
paymentReceived: { amount: number; currency: string };
};

class EventEmitter<T extends Record<string, unknown>> {
private handlers = new Map<keyof T, Set<(...args: unknown[]) => void>>();

on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}

emit<K extends keyof T>(event: K, data: T[K]): void {
this.handlers.get(event)?.forEach(handler => handler(data));
}
}

const emitter = new EventEmitter<EventMap>();
emitter.on("userCreated", data => {
console.log(data.name.toUpperCase()); // data is { name: string; email: string }
});
emitter.emit("userCreated", { name: "Alice", email: "alice@example.com" });

Pattern 2: The Builder Pattern​

class QueryBuilder<T extends Record<string, unknown>> {
private conditions: Partial<T> = {};

where<K extends keyof T>(key: K, value: T[K]): this {
this.conditions[key] = value;
return this;
}

build(): Partial<T> {
return { ...this.conditions };
}
}

interface UserQuery {
name: string;
age: number;
email: string;
}

const query = new QueryBuilder<UserQuery>()
.where("name", "Alice")
.where("age", 30)
.build();
// Type: Partial<UserQuery> — all properties are optional and correctly typed

Pattern 3: The Type-Safe API Client​

type ApiEndpoints = {
"/users": { get: User[]; post: User };
"/users/:id": { get: User; put: User; delete: void };
};

function apiClient<Path extends keyof ApiEndpoints>(
path: Path,
method: keyof ApiEndpoints[Path]
): Promise<ApiEndpoints[Path][typeof method]> {
return fetch(path, { method: method as string }).then(r => r.json());
}

const users = await apiClient("/users", "get"); // Type: User[]
const user = await apiClient("/users/:id", "get"); // Type: User

Try This: Generics​

  1. Write a generic function wrapInArray<T> that takes a value of type T and returns T[].
  2. Write a generic function merge<T, U> that takes two objects and returns their intersection.
  3. Write a generic Stack<T> class with push, pop, peek, and isEmpty methods.
  4. Write a generic function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> that returns a new object with only the specified keys.
  5. Write a generic Result<T> type and a function that uses it to wrap API calls.

Time needed: 25 minutes.

What to notice: How generics eliminate duplication. One firstElement function instead of three. One Stack class instead of StringStack, NumberStack, UserStack. How the type safety follows automatically — you never have to cast or assert.

The Revelation​

You now understand generics — the feature that makes TypeScript's type system truly expressive. But you've only scratched the surface. The type parameters you've used so far are simple: T, K, V. What if your types could make decisions? What if they could extract pieces of other types? What if they could transform one type into another?

That's what conditional types do. And the key that unlocks them is a little keyword called infer.


In the next chapter: generic interfaces, generic classes, and the infer keyword — how TypeScript can extract types from other types, and why that's the foundation of every utility type you'll ever use.