Complete quick reference for TypeScript generics -- type parameters, constraints, conditional types, mapped types, utility types, and real-world patterns with code examples.
Basic Generic Syntax
Syntax
Description
function fn<T>(arg: T): T
Generic function with one type parameter
class Box<T> { value: T; }
Generic class
interface Pair<A, B> { first: A; second: B; }
Generic interface
type Result<T> = { ok: true; data: T } | { ok: false; error: string }
Generic type alias
const fn = <T,>(x: T): T => x;
Generic arrow function (trailing comma avoids JSX ambiguity)
// Type parameter is inferred from argument
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T inferred as number
const str = identity("hello"); // T inferred as string
// Explicit type argument
const arr = identity<number[]>([1, 2, 3]);
Generic Constraints
Syntax
Description
T extends string
T must be assignable to string
T extends { id: number }
T must have an id property of type number
T extends keyof U
T must be a key of U
T extends (...args: any[]) => any
T must be a function type
T extends new (...args: any[]) => any
T must be a constructor (class) type
// Constrain T to objects with a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("abc", "de"); // OK: string has length
longest([1, 2], [1, 2, 3]); // OK: array has length
// keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const name = getProperty({ name: "Alice", age: 30 }, "name"); // string
Generic Functions
Pattern
Description
function map<T, U>(arr: T[], fn: (item: T) => U): U[]
Transform array elements
function filter<T>(arr: T[], pred: (item: T) => boolean): T[]
Filter array by predicate
function merge<T, U>(a: T, b: U): T & U
Merge two objects
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C
Compose two functions
// Generic merge function
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
const merged = merge({ name: "Alice" }, { age: 30 });
// Type: { name: string } & { age: number }
// Generic async wrapper
async function fetchData<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as Promise<T>;
}
const user = await fetchData<{ name: string }>("/api/user");
// Generic stack implementation
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 numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.pop(); // number | undefined
// Generic class with constraint
class KeyValueStore<K extends string | number, V> {
private store = new Map<K, V>();
set(key: K, value: V): void { this.store.set(key, value); }
get(key: K): V | undefined { return this.store.get(key); }
}
Generic Type Aliases
Type Alias
Description
type Nullable<T> = T | null
Make any type nullable
type ReadonlyArray<T> = readonly T[]
Immutable array type
type Callback<T> = (data: T) => void
Generic callback function
type AsyncResult<T> = Promise<Result<T>>
Composed generic types
type Tree<T> = { value: T; children: Tree<T>[] }
Recursive generic type
// Result type (Rust-inspired)
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: "Division by zero" };
return { ok: true, value: a / b };
}
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
Multiple Type Parameters
// Two type parameters
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((item, i) => [item, b[i]]);
}
const zipped = zip([1, 2], ["a", "b"]); // [number, string][]
// Three type parameters with constraints
function transform<TInput, TIntermediate, TOutput>(
input: TInput,
step1: (i: TInput) => TIntermediate,
step2: (i: TIntermediate) => TOutput
): TOutput {
return step2(step1(input));
}
// Convention: T, U, V or descriptive names TKey, TValue, TResult
type Dictionary<TKey extends string | number | symbol, TValue> = {
[K in TKey]: TValue;
};
Default Type Parameters
Syntax
Description
<T = string>
Default to string if not specified
<T extends object = {}>
Default with constraint
<T, U = T>
Default one param to another
<T = unknown, E = Error>
Multiple defaults
// Default type parameter
interface ApiClient<TResponse = unknown, TError = Error> {
get(url: string): Promise<TResponse>;
handleError(error: TError): void;
}
// Uses defaults: TResponse = unknown, TError = Error
const basic: ApiClient = { /* ... */ };
// Override just the first default
const typed: ApiClient<User> = { /* ... */ };
// Override both
const custom: ApiClient<User, ApiError> = { /* ... */ };
// Default referencing another param
function createPair<T, U = T>(first: T, second?: U): [T, U] {
return [first, (second ?? first) as U];
}
Conditional Types
Syntax
Description
T extends U ? X : Y
If T assignable to U, resolve to X; otherwise Y
T extends string ? "str" : "other"
Type-level ternary
T extends any[] ? T[number] : T
Extract element type or keep as-is
Distributive: T extends U ? X : Y
Distributes over unions when T is naked type param
[T] extends [U] ? X : Y
Non-distributive (wrapped in tuple)
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Distributive over unions
type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>; // string[] | number[]
// Non-distributive (wrapping prevents distribution)
type ToArrayND<T> = [T] extends [any] ? T[] : never;
type D = ToArrayND<string | number>; // (string | number)[]
// Nested conditionals
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends Function ? "function" : "object";
Mapped Types
Syntax
Description
{ [K in keyof T]: U }
Map over all keys of T
{ [K in keyof T]?: T[K] }
Make all properties optional
{ readonly [K in keyof T]: T[K] }
Make all properties readonly
{ [K in keyof T]-?: T[K] }
Remove optional modifier
{ -readonly [K in keyof T]: T[K] }
Remove readonly modifier
{ [K in keyof T as NewKey]: T[K] }
Key remapping (via as)
// Custom mapped types
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
// Filter keys by value type
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<{ name: string; age: number; email: string }>;
// { name: string; email: string }
Template Literal Types with Generics
// Event handler type generator
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
// CSS property builder
type CSSProperty<P extends string, V extends string> = `${P}: ${V}`;
// Route parameter extraction
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type RouteParams = ExtractParams<"/users/:id/posts/:postId">;
// { id: string; postId: string }
// Dot-notation path types
type PathOf<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? PathOf<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];
The infer Keyword
Pattern
Extracts
T extends (infer U)[] ? U : never
Array element type
T extends (...args: infer P) => any ? P : never
Function parameter types (tuple)
T extends (...args: any[]) => infer R ? R : never
Function return type
T extends Promise<infer U> ? U : T
Unwrap Promise value type
T extends new (...args: infer P) => infer I
Constructor params and instance type
T extends [infer First, ...infer Rest]
First element and remaining tuple
// Unwrap nested Promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type X = DeepAwaited<Promise<Promise<string>>>; // string
// Extract first argument
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string
// Tuple manipulation with infer
type Last<T extends any[]> =
T extends [...infer _, infer L] ? L : never;
type L = Last<[1, 2, 3]>; // 3
// String parsing with infer
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
Variance Annotations (in / out)
Annotation
Meaning
Use When
out T
Covariant
T only appears in output (return) positions
in T
Contravariant
T only appears in input (parameter) positions
in out T
Invariant
T appears in both input and output positions
// Covariant: T only in output position
interface Producer<out T> {
get(): T;
}
// Producer<Dog> is assignable to Producer<Animal> (if Dog extends Animal)
// Contravariant: T only in input position
interface Consumer<in T> {
accept(value: T): void;
}
// Consumer<Animal> is assignable to Consumer<Dog>
// Invariant: T in both positions
interface Store<in out T> {
get(): T;
set(value: T): void;
}
// Store<Dog> is NOT assignable to Store<Animal> or vice versa
// Combining utility types
interface User { id: string; name: string; email: string; role: "admin" | "user"; }
type CreateUserDTO = Omit<User, "id">;
type UpdateUserDTO = Partial<Omit<User, "id">>;
type UserSummary = Pick<User, "id" | "name">;
type UserLookup = Record<string, User>;
// ReturnType + Parameters
function greet(name: string, age: number): string {
return `Hello ${name}, you are ${age}`;
}
type GreetParams = Parameters<typeof greet>; // [string, number]
type GreetReturn = ReturnType<typeof greet>; // string
Frequently Asked Questions
What are generics in TypeScript?
Generics allow you to write reusable code that works with multiple types while preserving type safety. Instead of using any, you define type parameters (like T, U, K) that act as placeholders for actual types provided when the generic is used. This lets functions, classes, and interfaces adapt to different types without losing compile-time type checking.
What is the difference between generic constraints and conditional types?
Generic constraints (using extends) restrict what types can be passed as a type parameter, e.g., T extends string means T must be assignable to string. Conditional types (T extends U ? X : Y) choose between two types based on whether a condition is met at the type level. Constraints limit input types; conditional types compute output types based on type relationships.
When should I use the infer keyword in TypeScript?
The infer keyword is used inside conditional types to extract a type from within another type. Common uses include extracting return types from functions (ReturnType<T>), extracting element types from arrays, unwrapping Promise types, and extracting parameter types. It works only within the extends clause of a conditional type: T extends (...args: infer P) => any ? P : never.
What are mapped types in TypeScript and how do they work?
Mapped types create new types by transforming each property of an existing type. The syntax is { [K in keyof T]: NewType }. You can add or remove modifiers like readonly and optional (?). Built-in mapped types include Partial<T> (all optional), Required<T> (all required), Readonly<T> (all readonly), Pick<T, K> (subset of keys), and Record<K, V> (object from keys to values).
What are variance annotations (in/out) in TypeScript?
Variance annotations (in and out keywords on type parameters) were introduced in TypeScript 4.7. out T marks a type parameter as covariant (only used in output positions), and in T marks it as contravariant (only used in input positions). They help TypeScript check variance correctly and can improve type-checking performance by avoiding structural comparison.
How do I choose between a generic function and function overloads?
Use generics when the input and output types are related and you want a single implementation. Use function overloads when you have a small, fixed set of type combinations with different logic for each. Generics are more flexible and composable; overloads give more precise control over specific type mappings. When possible, prefer generics with conditional types over overloads for cleaner code.