TypeScript Generics Cheatsheet

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

SyntaxDescription
function fn<T>(arg: T): TGeneric 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

SyntaxDescription
T extends stringT must be assignable to string
T extends { id: number }T must have an id property of type number
T extends keyof UT must be a key of U
T extends (...args: any[]) => anyT must be a function type
T extends new (...args: any[]) => anyT 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

PatternDescription
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 & UMerge two objects
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => CCompose 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 Interfaces

// Generic collection interface
interface Collection<T> {
  items: T[];
  add(item: T): void;
  get(index: number): T | undefined;
  find(predicate: (item: T) => boolean): T | undefined;
}

// Generic response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  timestamp: Date;
}

// Callable interface with generics
interface Parser<TInput, TOutput> {
  parse(input: TInput): TOutput;
  tryParse(input: TInput): TOutput | null;
}

// Extending a generic interface
interface TimestampedResponse<T> extends ApiResponse<T> {
  cachedAt: Date;
}

Generic Classes

// 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 AliasDescription
type Nullable<T> = T | nullMake any type nullable
type ReadonlyArray<T> = readonly T[]Immutable array type
type Callback<T> = (data: T) => voidGeneric 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

SyntaxDescription
<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

SyntaxDescription
T extends U ? X : YIf T assignable to U, resolve to X; otherwise Y
T extends string ? "str" : "other"Type-level ternary
T extends any[] ? T[number] : TExtract element type or keep as-is
Distributive: T extends U ? X : YDistributes over unions when T is naked type param
[T] extends [U] ? X : YNon-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

SyntaxDescription
{ [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

PatternExtracts
T extends (infer U)[] ? U : neverArray element type
T extends (...args: infer P) => any ? P : neverFunction parameter types (tuple)
T extends (...args: any[]) => infer R ? R : neverFunction return type
T extends Promise<infer U> ? U : TUnwrap Promise value type
T extends new (...args: infer P) => infer IConstructor 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)

AnnotationMeaningUse When
out TCovariantT only appears in output (return) positions
in TContravariantT only appears in input (parameter) positions
in out TInvariantT 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

Common Patterns

// Factory pattern
function createFactory<T>(ctor: new (...args: any[]) => T): (...args: any[]) => T {
  return (...args) => new ctor(...args);
}

// Builder pattern
class QueryBuilder<T extends object> {
  private filters: Partial<T> = {};
  where<K extends keyof T>(key: K, value: T[K]): this {
    this.filters[key] = value;
    return this; // returns 'this' for chaining
  }
  build(): Partial<T> { return { ...this.filters }; }
}

// Repository pattern
interface Repository<T extends { id: string }> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(data: Omit<T, "id">): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

// Type-safe event emitter
class TypedEmitter<TEvents extends Record<string, any[]>> {
  private listeners = new Map<string, Function[]>();
  on<K extends keyof TEvents & string>(
    event: K, handler: (...args: TEvents[K]) => void
  ): void {
    const fns = this.listeners.get(event) ?? [];
    fns.push(handler);
    this.listeners.set(event, fns);
  }
  emit<K extends keyof TEvents & string>(event: K, ...args: TEvents[K]): void {
    this.listeners.get(event)?.forEach(fn => fn(...args));
  }
}

// Usage
type AppEvents = { login: [user: string]; error: [code: number, msg: string] };
const emitter = new TypedEmitter<AppEvents>();
emitter.on("login", (user) => {}); // user: string
emitter.on("error", (code, msg) => {}); // code: number, msg: string

Utility Types Reference

Utility TypeDescriptionExample
Partial<T>All properties optionalPartial<User> -- { name?: string; age?: number; }
Required<T>All properties requiredRequired<Partial<User>> -- removes all ?
Readonly<T>All properties readonlyReadonly<User> -- prevents mutation
Record<K, V>Object type with keys K and values VRecord<"a" | "b", number> -- { a: number; b: number }
Pick<T, K>Subset of T with only keys KPick<User, "name"> -- { name: string }
Omit<T, K>T without keys KOmit<User, "age"> -- { name: string }
Exclude<T, U>Remove U from union TExclude<"a" | "b" | "c", "a"> -- "b" | "c"
Extract<T, U>Keep only U from union TExtract<string | number, string> -- string
NonNullable<T>Remove null and undefinedNonNullable<string | null> -- string
Parameters<T>Function parameter types as tupleParameters<typeof fn> -- [string, number]
ReturnType<T>Function return typeReturnType<typeof fn> -- boolean
InstanceType<T>Instance type of a constructorInstanceType<typeof MyClass>
Awaited<T>Recursively unwrap PromiseAwaited<Promise<string>> -- string
ThisParameterType<T>Extract this parameter typeThisParameterType<typeof fn>
NoInfer<T>Block inference at this position (TS 5.4+)Prevents widening of literal types
// 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.