TypeScript Generics: The Complete Guide for 2026
Generics are the most powerful feature in TypeScript's type system. They let you write functions, classes, and types that work with any data type while preserving full type safety — no any escape hatches needed. This guide takes you from your first generic function all the way through conditional types, recursive types, variadic tuples, and production patterns used in modern React and API codebases.
1. What Are Generics and Why You Need Them
Imagine you need a function that wraps any value in an array. Without generics, you have two bad options: use any and lose type safety, or write a separate function for every type. Generics solve this with a type parameter that gets filled in when the function is called:
// BAD: any loses all type information
function wrapInArray(value: any): any[] { return [value]; }
const result = wrapInArray("hello"); // any[] -- no type safety
// GOOD: generic preserves the type
function wrapInArray<T>(value: T): T[] { return [value]; }
const strings = wrapInArray("hello"); // string[]
const numbers = wrapInArray(42); // number[]
const users = wrapInArray({ name: "Alice", age: 30 }); // { name: string; age: number }[]
The <T> declares a type variable. TypeScript infers T from the argument you pass in, then uses that concrete type everywhere T appears. You get full type safety, autocomplete, and error checking — without duplicating any code.
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = firstElement([1, 2, 3]); // number | undefined
const s = firstElement(["a", "b"]); // string | undefined
// n.toFixed(2) -- TypeScript knows this is a number method
// s.toUpperCase() -- TypeScript knows this is a string method
2. Generic Functions
Type Parameters
Generic functions declare type parameters in angle brackets. By convention: T for type, K for key, V for value, E for element.
// Single type parameter
function identity<T>(value: T): T { return value; }
// Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] { return [first, second]; }
const p = pair("hello", 42); // [string, number]
// Arrow function syntax (trailing comma avoids JSX ambiguity)
const identity2 = <T,>(value: T): T => value;
// Explicit type when inference isn't enough
const result = identity<string>("hello");
Multiple Type Parameters
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const lengths = map(["hello", "world"], s => s.length); // number[]
const names = map([1, 2, 3], n => `Item ${n}`); // string[]
function swap<A, B>(tuple: [A, B]): [B, A] { return [tuple[1], tuple[0]]; }
const swapped = swap(["hello", 42]); // [number, string]
Default Type Parameters
type Container<T = string> = { value: T; timestamp: Date };
const c1: Container = { value: "hello", timestamp: new Date() }; // T defaults to string
const c2: Container<number> = { value: 42, timestamp: new Date() }; // T is number
type EventHandler<T = void> = (payload: T) => void;
const onClick: EventHandler = () => console.log("clicked"); // payload is void
const onData: EventHandler<string> = (data) => console.log(data); // payload is string
3. Generic Interfaces and Types
Generics shine when defining reusable data structures. Define the shape once, parameterize the varying parts.
// Generic API response
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type UserResponse = ApiResponse<{ id: string; name: string; email: string }>;
type ProductListResponse = ApiResponse<{ products: Product[]; total: number }>;
// Generic result pattern (like Rust's Result)
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJSON<T>(json: string): Result<T> {
try {
return { ok: true, value: JSON.parse(json) };
} catch (e) {
return { ok: false, error: e as Error };
}
}
const result = parseJSON<{ name: string }>('{"name": "Alice"}');
if (result.ok) console.log(result.value.name); // TypeScript knows .name exists
Generic Type Aliases and Extension
// Recursive generic types
type TreeNode<T> = { value: T; children: TreeNode<T>[] };
type Dictionary<T> = { [key: string]: T };
const scores: Dictionary<number> = { alice: 95, bob: 87 };
// Extending generic interfaces
interface Identifiable { id: string; }
interface Timestamped { createdAt: Date; updatedAt: Date; }
interface Entity<T> extends Identifiable, Timestamped {
data: T;
}
type UserEntity = Entity<{ name: string; email: string }>;
// { id: string; createdAt: Date; updatedAt: Date; data: { name: string; email: string } }
4. Generic Classes
Generic classes let you build type-safe data structures that work with any entity 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 numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
// numberStack.push("hello"); // Error: string is not assignable to number
Generic Repository Pattern
interface HasId { id: string; }
class Repository<T extends HasId> {
private items = new Map<string, T>();
create(item: T): T { this.items.set(item.id, item); return item; }
findById(id: string): T | undefined { return this.items.get(id); }
findAll(): T[] { return Array.from(this.items.values()); }
update(id: string, updates: Partial<Omit<T, "id">>): T | undefined {
const existing = this.items.get(id);
if (!existing) return undefined;
const updated = { ...existing, ...updates };
this.items.set(id, updated);
return updated;
}
delete(id: string): boolean { return this.items.delete(id); }
}
interface User extends HasId { name: string; email: string; }
const userRepo = new Repository<User>();
userRepo.create({ id: "1", name: "Alice", email: "alice@example.com" });
5. Generic Constraints with extends
The extends keyword constrains what types a generic parameter can accept, guaranteeing that certain properties or methods exist.
// Without constraint: error -- T might not have .length
// function logLength<T>(value: T) { console.log(value.length); } // Error!
// With constraint: T must have a length property
function logLength<T extends { length: number }>(value: T): void {
console.log(value.length); // OK
}
logLength("hello"); // OK: string has .length
logLength([1, 2, 3]); // OK: array has .length
// logLength(42); // Error: number doesn't have .length
// Constraining one 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 };
const name = getProperty(user, "name"); // string
// getProperty(user, "phone"); // Error: "phone" is not in keyof user
Multiple Constraints
interface Serializable { serialize(): string; }
interface Loggable { log(): void; }
// Intersection for multiple constraints
function process<T extends Serializable & Loggable>(item: T): string {
item.log();
return item.serialize();
}
// Constructor constraint
type Constructor<T = any> = new (...args: any[]) => T;
function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
return new ctor(...args);
}
6. keyof and Mapped Types with Generics
The keyof operator produces a union of property names. Combined with generics, it enables type-safe transformations.
interface User { id: number; name: string; email: string; isActive: boolean; }
type UserKeys = keyof User; // "id" | "name" | "email" | "isActive"
// Type-safe pick function
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const subset = pick(user, "name", "email"); // { name: string; email: string }
Mapped Types
// How Partial is implemented:
type MyPartial<T> = { [K in keyof T]?: T[K] };
// How Readonly is implemented:
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Make all properties nullable:
type Nullable<T> = { [K in keyof T]: T[K] | null };
Key Remapping with as
// Create getter methods
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
// Filter properties by value type
type OnlyStringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringUserProps = OnlyStringProps<User>; // { name: string; email: string }
// Generate event handler types
type StateEvents<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (newValue: T[K]) => void;
};
7. Conditional Types and infer
Conditional types use T extends U ? X : Y — a ternary operator for the type system.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
The infer Keyword
infer declares a type variable within a conditional type that TypeScript figures out from context:
// How ReturnType works internally:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = MyReturnType<(x: string) => number>; // number
// Extract element type from array:
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Item = ElementOf<string[]>; // string
// Extract Promise resolved type:
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
// Extract first parameter type:
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type First = FirstParam<(name: string, age: number) => void>; // string
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type Distributed = ToArray<string | number>; // string[] | number[]
// Prevent distribution by wrapping in a tuple:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type NonDistributed = ToArrayNonDist<string | number>; // (string | number)[]
8. Built-in Utility Types That Use Generics
TypeScript ships with utility types built on generics. Understanding their implementations deepens your understanding of generics. For a deeper dive, see our TypeScript Utility Types Guide.
// Partial<T> -- make all properties optional
type Partial<T> = { [K in keyof T]?: T[K] };
// Required<T> -- make all properties required
type Required<T> = { [K in keyof T]-?: T[K] };
// Pick<T, K> -- select specific properties
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit<T, K> -- remove specific properties
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// Record<K, V> -- create a dictionary type
type Record<K extends keyof any, T> = { [P in K]: T };
// Exclude<T, U> -- remove union members
type Exclude<T, U> = T extends U ? never : T;
// Extract<T, U> -- keep matching union members
type Extract<T, U> = T extends U ? T : never;
Practical Combinations
interface Article {
id: string; title: string; body: string;
author: string; publishedAt: Date;
}
type CreateArticle = Omit<Article, "id" | "publishedAt">;
type UpdateArticle = Partial<Pick<Article, "title" | "body">>;
type ArticlePreview = Pick<Article, "id" | "title" | "author">;
type Status = "pending" | "active" | "archived";
const labels: Record<Status, string> = {
pending: "Waiting", active: "Live", archived: "Removed"
// Missing a status? TypeScript reports an error
};
type Events = "click" | "scroll" | "keydown" | "keyup";
type KeyEvents = Extract<Events, "keydown" | "keyup">; // "keydown" | "keyup"
type NonKeyEvents = Exclude<Events, "keydown" | "keyup">; // "click" | "scroll"
9. Generic React Components
Generic components are essential for reusable UI libraries, providing full type safety to consumers.
Generic List Component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// T is inferred as User -- full autocomplete on 'u'
interface User { id: string; name: string; email: string; }
<List items={users} keyExtractor={u => u.id} renderItem={u => <span>{u.name}</span>} />
Generic Select Component
interface SelectProps<T> {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={value ? getValue(value) : ""}
onChange={e => {
const selected = options.find(o => getValue(o) === e.target.value);
if (selected) onChange(selected);
}}
>
{options.map(opt => (
<option key={getValue(opt)} value={getValue(opt)}>{getLabel(opt)}</option>
))}
</select>
);
}
// Full type safety for any data shape
<Select options={countries} value={selected} onChange={setSelected}
getLabel={c => c.name} getValue={c => c.code} />
Generic forwardRef
import { Ref } from "react";
interface InputProps<T> {
value: T;
onChange: (value: T) => void;
parse: (raw: string) => T;
format: (value: T) => string;
}
// Wrapper pattern to preserve the generic with forwardRef
function GenericInput<T>(props: InputProps<T> & { ref?: Ref<HTMLInputElement> }) {
return (
<input ref={props.ref} value={props.format(props.value)}
onChange={e => props.onChange(props.parse(e.target.value))} />
);
}
10. Generic API Patterns
Type-Safe Fetch Wrapper
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: { message: string; code: number } };
async function apiFetch<T>(url: string, options?: RequestInit): Promise<ApiResult<T>> {
try {
const response = await fetch(url, {
headers: { "Content-Type": "application/json" }, ...options,
});
if (!response.ok) {
const err = await response.json();
return { ok: false, error: { message: err.error, code: response.status } };
}
const data: T = await response.json();
return { ok: true, data };
} catch (e) {
return { ok: false, error: { message: "Network error", code: 0 } };
}
}
// Return type is fully typed
const result = await apiFetch<User[]>("/api/users");
if (result.ok) result.data.forEach(user => console.log(user.name));
Generic CRUD Client
interface CrudApi<T extends { id: string }> {
getAll(): Promise<T[]>;
getById(id: string): Promise<T>;
create(data: Omit<T, "id">): Promise<T>;
update(id: string, data: Partial<Omit<T, "id">>): Promise<T>;
remove(id: string): Promise<void>;
}
function createCrudApi<T extends { id: string }>(baseUrl: string): CrudApi<T> {
return {
getAll: () => apiFetch<T[]>(baseUrl).then(r => { if (r.ok) return r.data; throw r.error; }),
getById: (id) => apiFetch<T>(`${baseUrl}/${id}`).then(r => { if (r.ok) return r.data; throw r.error; }),
create: (data) => apiFetch<T>(baseUrl, { method: "POST", body: JSON.stringify(data) }).then(r => { if (r.ok) return r.data; throw r.error; }),
update: (id, data) => apiFetch<T>(`${baseUrl}/${id}`, { method: "PATCH", body: JSON.stringify(data) }).then(r => { if (r.ok) return r.data; throw r.error; }),
remove: (id) => apiFetch<void>(`${baseUrl}/${id}`, { method: "DELETE" }).then(() => {}),
};
}
// Fully typed API clients
const usersApi = createCrudApi<User>("/api/users");
const users = await usersApi.getAll(); // User[]
Typed Endpoint Map
interface ApiEndpoints {
"/users": { response: User[]; params: { role?: string } };
"/users/:id": { response: User; params: { id: string } };
"/products": { response: Product[]; params: { category?: string } };
}
async function typedFetch<E extends keyof ApiEndpoints>(
endpoint: E, params?: ApiEndpoints[E]["params"]
): Promise<ApiEndpoints[E]["response"]> {
const response = await fetch(endpoint as string);
return response.json();
}
const users = await typedFetch("/users", { role: "admin" }); // User[]
11. Advanced Patterns
Recursive Types
// JSON value -- recursive definition
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
// Deep partial -- makes ALL nested properties optional
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Array<any> ? T[K] : DeepPartial<T[K]>
: T[K];
};
// Recursive dot-notation path type
type Path<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? Path<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
: `${Prefix}${K}`;
}[keyof T & string];
interface Config { server: { host: string; port: number }; db: { url: string } }
type ConfigPath = Path<Config>;
// "server" | "server.host" | "server.port" | "db" | "db.url"
Variadic Tuple Types
type Concat<A extends any[], B extends any[]> = [...A, ...B];
type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
// Typed pipe function
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B;
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C;
function pipe<A, B, C, D>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): (a: A) => D;
function pipe(...fns: Function[]) {
return (arg: any) => fns.reduce((acc, fn) => fn(acc), arg);
}
const transform = pipe(
(s: string) => s.length, // string => number
(n: number) => n > 5, // number => boolean
); // (s: string) => boolean
Template Literal Types with Generics
// Type-safe event emitter
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; code: number };
resize: { width: number; height: number };
};
class TypedEmitter<Events extends Record<string, any>> {
private handlers = new Map<string, Function[]>();
on<E extends keyof Events & string>(event: E, handler: (payload: Events[E]) => void): void {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit<E extends keyof Events & string>(event: E, payload: Events[E]): void {
this.handlers.get(event)?.forEach(fn => fn(payload));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("click", ({ x, y }) => console.log(`${x}, ${y}`)); // payload typed
emitter.on("keydown", ({ key }) => console.log(key)); // payload typed
// emitter.emit("click", { key: "a" }); // Error: missing x and y
12. Common Mistakes and Debugging Generic Errors
Mistake 1: Over-Generalizing
// BAD: unnecessary generic -- T is not reused
function greet<T extends string>(name: T): string { return `Hello, ${name}`; }
// GOOD: just use the type directly
function greet(name: string): string { return `Hello, ${name}`; }
// RULE: Only use a generic when the type parameter appears in 2+ places
Mistake 2: Forgetting Constraints
// BAD: T has no constraints
function getId<T>(item: T): string { return item.id; } // Error!
// GOOD: constrain T
function getId<T extends { id: string }>(item: T): string { return item.id; }
Mistake 3: Object Literal Widening
function getConfig<T>(value: T): T { return value; }
const a = getConfig("dark"); // type is "dark" (literal)
const b = getConfig({ theme: "dark" }); // { theme: string } -- widened!
const c = getConfig({ theme: "dark" } as const); // { readonly theme: "dark" } -- preserved
Mistake 4: Not Understanding Distribution
type IsString<T> = T extends string ? "yes" : "no";
type Test = IsString<string | number>; // "yes" | "no" -- distributes over union!
// Prevent distribution with tuple wrapping:
type IsStringStrict<T> = [T] extends [string] ? "yes" : "no";
type Test2 = IsStringStrict<string | number>; // "no"
Debugging Tips
// 1. Prettify helper -- hover to see expanded types
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type Debug = Prettify<Pick<User, "name"> & { extra: boolean }>;
// Shows: { name: string; extra: boolean }
// 2. Use 'satisfies' to check without widening (TS 4.9+)
const config = {
host: "localhost", port: 3000,
} satisfies Record<string, string | number>;
// config.port is still 'number', not 'string | number'
// 3. Replace generics with concrete types to isolate errors
// Debug: function process(items: User[]): User[]
// Then re-add generics once the logic works
The most important rule: generics should make your code safer, not more complicated. If a generic type makes code harder to read without adding real safety, simplify it. Use our JSON Formatter to validate API response structures, and the JSON to TypeScript Converter to generate the base types your generics will transform.
Frequently Asked Questions
What are generics in TypeScript and why should I use them?
Generics are type parameters that let you write functions, classes, and interfaces that work with any type while preserving type safety. Instead of using any and losing all type checking, generics capture the actual type and carry it through your code. Use generics whenever you have reusable logic that should work across multiple types without sacrificing type information.
What is the difference between generics and the any type?
The any type disables type checking entirely. Generics preserve the actual type: identity<string>("hello") returns string with full intellisense. With any, the return would be any and you lose all safety. Generics give flexibility without sacrificing the compiler's ability to catch bugs.
How do generic constraints work with extends?
The extends keyword limits what types a generic accepts. <T extends { length: number }> means T must have a length property. The keyof operator is commonly used: <T, K extends keyof T> ensures K is a valid key of T, enabling type-safe property access like getProperty(obj, "name").
What are conditional types and how does infer work?
Conditional types use T extends U ? X : Y to branch at the type level. The infer keyword declares a type variable that TypeScript fills in from context. For example, T extends Array<infer E> ? E : never extracts the element type from an array. This is what powers ReturnType, Parameters, and Awaited.
How do I write generic React components?
Add a type parameter to the function: function List<T>(props: { items: T[]; renderItem: (item: T) => ReactNode }). Callers get full inference: <List items={users} renderItem={user => user.name} /> infers T as User. For arrow functions in TSX, use <T,> with a trailing comma to avoid JSX ambiguity.
Related Resources
- TypeScript Utility Types Guide — deep dive into every built-in utility type
- TypeScript Tips and Tricks — 15 practical patterns for cleaner code
- TypeScript Complete Guide — comprehensive guide from basics to advanced
- TypeScript Types Cheat Sheet — quick reference for all built-in types
- JSON to TypeScript Converter — generate interfaces from JSON data
- JSON Formatter — format and validate JSON for TypeScript projects
Keep learning: Generics are the foundation for TypeScript's advanced type system. Our TypeScript Utility Types Guide shows how every built-in utility type is built on the generic patterns covered here. Use the TypeScript Types Cheat Sheet as a quick reference while coding.