TypeScript Cheatsheet

Generics

Use this TypeScript reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Generic Functions

// Single type parameter
function identity<T>(value: T): T {
  return value;
}
identity<string>("hello");  // explicit
identity(42);               // inferred: T = number

// Multiple type parameters
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}
pair("hello", 42); // [string, number]

// Generic arrow function (in .tsx, add trailing comma to disambiguate from JSX)
const wrap = <T,>(val: T): { value: T } => ({ value: val });

// Return type inferred from parameter
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

Generic Constraints

// extends constrains T to a shape
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}
getLength("hello");    // OK
getLength([1, 2, 3]);  // OK
getLength(42);         // Error: number has no 'length'

// keyof constraint — key must be a property of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // string
getProperty(user, "age");  // number

// Multiple constraints via intersection
function processItem<T extends Serializable & Loggable>(item: T): void {
  item.serialize();
  item.log();
}

// Constrain to primitive or specific types
function toArray<T extends string | number | boolean>(val: T): T[] {
  return [val];
}

Default Type Parameters

// Default applied when type is not inferred or provided
interface Container<T = string> {
  value: T;
}
const c1: Container = { value: "hello" };       // T = string
const c2: Container<number> = { value: 42 };    // T = number

// Default with constraint
function createList<T extends object = Record<string, unknown>>(): T[] {
  return [];
}

// Multiple defaults
type ApiResponse<T = unknown, E = Error> =
  | { ok: true; data: T }
  | { ok: false; error: E };

Generic Interfaces

interface Repository<T> {
  findById(id: number): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

interface Pair<A, B> {
  first: A;
  second: B;
  swap(): Pair<B, A>;
}

// Generic method inside non-generic interface
interface Converter {
  convert<T, U>(value: T, fn: (v: T) => U): U;
}

Generic Classes

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

  push(item: T): this { this.items.push(item); return this; }
  pop(): T | undefined { return this.items.pop(); }
  peek(): T | undefined { return this.items.at(-1); }
  isEmpty(): boolean { return this.items.length === 0; }
  get size(): number { return this.items.length; }
  toArray(): T[] { return [...this.items]; }
}

const stack = new Stack<number>();
stack.push(1).push(2).push(3);
stack.peek(); // 3
stack.pop();  // 3

// Generic class with constraint
class SortedList<T extends { compareTo(other: T): number }> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
    this.items.sort((a, b) => a.compareTo(b));
  }

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

Generic Type Aliases

// Simple alias
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type Maybe<T> = T | null | undefined;

// Higher-order generic
type Mapper<T, U> = (value: T, index: number, arr: T[]) => U;
type Predicate<T> = (value: T) => boolean;
type Reducer<T, R> = (acc: R, cur: T) => R;

// Result / Either type
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// Record-like with constraint
type Indexed<T, K extends string | number | symbol = string> = Record<K, T>;

// Recursive generic
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

Conditional Types with Generics

// Basic conditional
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<number>;   // false

// Unwrap array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string

// Flatten union of arrays
type Flatten<T> = T extends Array<infer U> ? U : T;
type F = Flatten<number[] | string>; // number | string

// NonNullable implementation
type MyNonNullable<T> = T extends null | undefined ? never : T;

// Unwrap Promise
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;
type Aw = Awaited<Promise<Promise<string>>>; // string

// Distribute over union
type ToArray<T> = T extends any ? T[] : never;
type TA = ToArray<string | number>; // string[] | number[]

// Prevent distribution with []
type ToArrayStrict<T> = [T] extends [any] ? T[] : never;
type TAS = ToArrayStrict<string | number>; // (string | number)[]

Mapped Types with Generics

// Pick implementation
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Omit implementation
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;

// Partial implementation
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Required implementation
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Readonly implementation
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Record implementation
type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

// Remap keys with `as`
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

infer Keyword

// Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Extract parameter types as tuple
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// Extract first parameter
type FirstParameter<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

// Extract constructor parameters
type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never;

// Extract instance type
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;

// Extract promise value type
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;

// Multiple infer in one condition
type Swap<T extends [any, any]> = T extends [infer A, infer B] ? [B, A] : never;
type S = Swap<[string, number]>; // [number, string]

// Infer in union
type UnionToTuple<T> = // complex but useful for exhaustive mapping
  [T] extends [never] ? [] : never; // simplified

Generic Utility Patterns

// Builder pattern
class QueryBuilder<T extends object> {
  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 };
  }
}

// Generic cache
class Cache<K, 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); }
  getOrSet(key: K, factory: () => V): V {
    if (!this.store.has(key)) this.store.set(key, factory());
    return this.store.get(key)!;
  }
}

// Event emitter
type EventMap = Record<string, unknown[]>;
class TypedEmitter<Events extends EventMap> {
  private listeners = new Map<keyof Events, ((...args: any[]) => void)[]>();

  on<K extends keyof Events>(event: K, fn: (...args: Events[K]) => void): this {
    (this.listeners.get(event) ?? (this.listeners.set(event, []), this.listeners.get(event)!)).push(fn);
    return this;
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.listeners.get(event)?.forEach(fn => fn(...args));
  }
}

Variance and Covariance

// Covariant position (return types) — subtype is assignable to supertype
type Producer<T> = () => T;
// Producer<Dog> is assignable to Producer<Animal> (if Dog extends Animal)

// Contravariant position (parameter types) — supertype is assignable to subtype
type Consumer<T> = (val: T) => void;
// Consumer<Animal> is assignable to Consumer<Dog>

// in/out variance markers (TS 4.7+) — help with complex generics
interface Box<out T> {   // covariant: only produces T
  get(): T;
}
interface Sink<in T> {   // contravariant: only consumes T
  put(val: T): void;
}

Generic Constraints with extends and Conditional

// Ensure T has a particular method
function callMethod<T extends { toString(): string }>(val: T): string {
  return val.toString();
}

// Require T to be a constructor
function createInstance<T>(ctor: new () => T): T {
  return new ctor();
}

// Narrowing generics
function processValue<T>(val: T): T extends string ? number : string {
  if (typeof val === "string") return val.length as any;
  return String(val) as any;
}

// Conditional generic with default
type Prettify<T> = { [K in keyof T]: T[K] } & {};
// Forces TS to expand the type when hovering — useful for debugging complex types

Variadic Tuple Types

// Spread in tuple types
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type AB = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

// Prepend
type Prepend<H, T extends unknown[]> = [H, ...T];
type P = Prepend<string, [number, boolean]>; // [string, number, boolean]

// Generic rest parameters (TS 4.0+)
function zip<T extends unknown[], U extends unknown[]>(
  t: [...T],
  u: [...U]
): { [K in keyof T]: [T[K], K extends keyof U ? U[K] : never] } {
  return t.map((v, i) => [v, u[i]]) as any;
}