Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. TypeScript
  4. TypeScript Generics Cheat Sheet
TypeScriptCheat Sheet

TypeScript Generics Cheat Sheet

Constraints, defaults, keyof patterns, conditional types, and common generic utilities -- all in one reference.

On this page
  1. 1Basic Generic Function
  2. 2Generic Interfaces and Types
  3. 3Generic Constraints (extends)
  4. 4keyof + Generic Constraint Pattern
  5. 5Multiple Type Parameters
  6. 6Generic Default Types
  7. 7Generic Utility Patterns
  8. 8Conditional Types with Generics (infer)
  9. 9Generic Gotchas
Basic Generic FunctionGeneric Interfaces and TypesGeneric Constraints (extends)keyof + Generic Constraint PatternMultiple Type ParametersGeneric Default TypesGeneric Utility PatternsConditional Types with Generics (infer)Generic Gotchas

Basic Generic Function

A type parameter captures the type at the call site and carries it through the function signature.

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

const num = identity(42);        // => type: number
const str = identity('hello');   // => type: string

TypeScript infers T from the argument. You rarely need to pass it explicitly.

Generic arrow function
// In .tsx files, add a trailing comma to avoid JSX ambiguity
const toArray = <T,>(value: T): T[] => [value];

toArray(5);    // => type: number[]
toArray('a');  // => type: string[]
Explicit type argument
function parse<T>(raw: string): T {
  return JSON.parse(raw) as T;
}

const user = parse<{ name: string; age: number }>('{"name":"Alice","age":30}');
// user.name => type: string

Explicit type arguments are needed when TypeScript can't infer T from the parameters.

Generic Interfaces and Types

Generics work on interfaces and type aliases the same way they work on functions.

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

const userResponse: ApiResponse<{ name: string }> = {
  data: { name: 'Alice' },
  status: 200,
  timestamp: new Date(),
};
Generic type alias
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const success: Result<number> = { ok: true, value: 42 };
const failure: Result<number> = { ok: false, error: new Error('oops') };
Generic class
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.at(-1); }
}

const stack = new Stack<number>();
stack.push(10);
stack.peek();  // => type: number | undefined

Generic Constraints (extends)

Use `extends` to restrict which types a generic parameter accepts.

Require a property
function logLength<T extends { length: number }>(item: T): void {
  console.log(item.length);
}

logLength('hello');     // => OK: string has .length
logLength([1, 2, 3]);  // => OK: array has .length
// logLength(42);       // => Error: number has no .length
Constrain to specific types
function formatId<T extends string | number>(id: T): string {
  return `ID-${id}`;
}

formatId(123);       // => 'ID-123'
formatId('abc');     // => 'ID-abc'
// formatId(true);   // => Error: boolean not assignable to string | number

keyof + Generic Constraint Pattern

The most common generic pattern: safely access object properties with full type inference.

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

const user = { id: 1, name: 'Alice', role: 'admin' as const };

getProperty(user, 'name');   // => type: string
getProperty(user, 'role');   // => type: 'admin'
// getProperty(user, 'foo'); // => Error: 'foo' not in 'id' | 'name' | 'role'
Pick multiple properties
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map((key) => obj[key]);
}

const config = { host: 'localhost', port: 3000, debug: true };
pluck(config, ['host', 'port']);  // => type: (string | number)[]

Multiple Type Parameters

Use multiple type parameters when a function relates two or more types.

Map function with two type params
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const lengths = mapArray(['hello', 'world'], (s) => s.length);
// lengths => type: number[]
Pair two values
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const p = pair('name', 42);  // => type: [string, number]

Generic Default Types

Defaults let callers omit type arguments when a sensible fallback exists.

Default type parameter
interface PaginatedList<T, Meta = { page: number; total: number }> {
  items: T[];
  meta: Meta;
}

// Uses default Meta type
const users: PaginatedList<{ name: string }> = {
  items: [{ name: 'Alice' }],
  meta: { page: 1, total: 50 },
};

// Overrides Meta type
const tagged: PaginatedList<string, { cursor: string }> = {
  items: ['a', 'b'],
  meta: { cursor: 'abc123' },
};

Default type parameters must come after required ones, just like default function parameters.

Event emitter with defaults
type EventMap = Record<string, unknown>;

class Emitter<Events extends EventMap = Record<string, never>> {
  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    // implementation
  }
}

interface AppEvents {
  login: { userId: string };
  logout: undefined;
}

const bus = new Emitter<AppEvents>();
bus.emit('login', { userId: '123' });   // => OK
// bus.emit('login', { wrong: true });   // => Error

Generic Utility Patterns

Common generic patterns you'll see (and write) in real TypeScript codebases.

Type-safe Object.keys
function typedKeys<T extends object>(obj: T): (keyof T)[] {
  return Object.keys(obj) as (keyof T)[];
}

const settings = { theme: 'dark', fontSize: 14 };
const keys = typedKeys(settings);  // => type: ('theme' | 'fontSize')[]

Object.keys returns string[] by design. This wrapper trades runtime safety for type convenience -- only use it when you control the object shape.

Builder pattern with generics
class QueryBuilder<T> {
  private filters: Partial<T> = {};

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

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

interface User { name: string; age: number; active: boolean }

const query = new QueryBuilder<User>()
  .where('age', 30)
  .where('active', true)
  .build();
// query => type: Partial<User>

Conditional Types with Generics (infer)

Conditional types let you create types that depend on a condition, and `infer` extracts types from within other types.

Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>;  // => true
type B = IsString<42>;       // => false
type C = IsString<string>;   // => true
Extract with infer
// Extract the return type of a function
type GetReturn<T> = T extends (...args: unknown[]) => infer R ? R : never;

type A = GetReturn<() => string>;           // => string
type B = GetReturn<(x: number) => boolean>; // => boolean

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

type C = ElementOf<string[]>;   // => string
type D = ElementOf<number[]>;   // => number

The built-in ReturnType<T> and Parameters<T> use this exact infer pattern under the hood.

Distributive conditional types
type ToArray<T> = T extends unknown ? T[] : never;

// Distributes over union members
type A = ToArray<string | number>;  // => string[] | number[]

// Prevent distribution with []
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayNonDist<string | number>;  // => (string | number)[]

Generic Gotchas

Common mistakes and surprising behavior with TypeScript generics.

Type erasure at runtime
function isArrayOf<T>(arr: unknown[]): arr is T[] {
  // T doesn't exist at runtime -- you can't check it here
  // This compiles but does nothing useful:
  return Array.isArray(arr); // Always true for any array
}

// Instead, pass a type guard as a parameter:
function isArrayOfType<T>(
  arr: unknown[],
  guard: (item: unknown) => item is T
): arr is T[] {
  return arr.every(guard);
}

Generic type parameters are erased at compile time. You can't use T in runtime checks like typeof or instanceof.

Constraint mismatch with objects
// This looks correct but fails:
function merge<T extends object>(a: T, b: T): T {
  return { ...a, ...b };
  // Error: spread type can only be created from object types
  // The spread produces { ...T, ...T } which TS can't verify is T
}

// Fix: widen the return type
function merge<T extends object>(a: T, b: Partial<T>): T {
  return { ...a, ...b } as T;
}
Inference fails with overloads
// Overloads don't infer generics from implementation
function wrap<T>(value: T): { wrapped: T };
function wrap(value: unknown): { wrapped: unknown } {
  return { wrapped: value };
}

const result = wrap(42);  // => type: { wrapped: number }

TypeScript picks the first overload signature whose parameters match. Put more specific overloads first.

Learn TypeScript in Depth
Generics →
Warm-up1 / 1

Can you write this from memory?

Write a generic identity function that takes type T and returns T

See Also
Utility Types →Interface vs Type →

Start Practicing TypeScript

Free daily exercises with spaced repetition. No credit card required.

← Back to TypeScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.