Skip to content
intermediate Phase 2 · TypeScript Advanced Types

Generics

Write reusable, type-safe code with generics and generic constraints.

1h 15m
0 problems
Topic Progress 0%

Generic Functions

Generic Functions

Generics allow you to write reusable code that works with any type while preserving type safety.

Basic Generic Function

// Without generics - lose type information
function identity(value: any): any {
  return value;
}
const result = identity('hello'); // type: any (bad)

// With generics - preserve type information
function identity<T>(value: T): T {
  return value;
}
const result2 = identity('hello'); // type: string (good)
const result3 = identity(42);      // type: number

Multiple Type Parameters

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair('hello', 42);       // [string, number]
const p2 = pair(true, [1, 2, 3]);  // boolean, number[]

// Generic function with callback
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
  return array.map(fn);
}

const doubled = map([1, 2, 3], (n) => n * 2); // number[]
const strings = map([1, 2, 3], (n) => String(n)); // string[]

Generic Constraints

Generic Constraints

Constrain type parameters to have certain properties using the extends keyword.

Basic Constraints

// T must have a 'length' property
function logLength<T extends { length: number }>(item: T): void {
  console.log(`Length: ${item.length}`);
}

logLength('hello');     // OK: string has length
logLength([1, 2, 3]);   // OK: array has length
logLength({ length: 5, name: 'test' }); // OK
// logLength(42);       // Error: number has no length

keyof Constraint

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

const person = { name: 'Alice', age: 30, email: 'alice@test.com' };

getProperty(person, 'name');  // type: string
getProperty(person, 'age');   // type: number
// getProperty(person, 'phone'); // Error: 'phone' not in keyof person

Class Constraints

class Container<T> {
  private value: T;
  constructor(value: T) {
    this.value = value;
  }
  getValue(): T {
    return this.value;
  }
}

const strContainer = new Container('hello');
const numContainer = new Container(42);

// Constraint: T must be array-like
function first<T extends { [index: number]: unknown; length: number }>(arr: T): T[0] {
  return arr[0];
}

first([1, 2, 3]);      // number
first(['a', 'b']);     // string

Generic Interfaces and Classes

Generic Interfaces and Classes

Generic Interfaces

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

// Typed API response
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: 'Alice', email: 'alice@test.com' },
  status: 200,
  message: 'Success',
  timestamp: new Date()
};

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(entity: Omit<T, 'id'>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T | null>;
  delete(id: string): Promise<boolean>;
}

Generic Classes

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];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined

const stringStack = new Stack<string>();
stringStack.push('hello');

Generic Defaults and Advanced Patterns

Generic Defaults and Advanced Patterns

Generic Defaults

// Default type parameter
interface PaginatedResponse<T = unknown> {
  data: T[];
  page: number;
  pageSize: number;
  total: number;
}

// Uses default type
const response: PaginatedResponse = {
  data: [1, 2, 3],
  page: 1,
  pageSize: 10,
  total: 100
};

// Overrides default type
const userResponse: PaginatedResponse<User> = {
  data: [{ id: 1, name: 'Alice', email: 'alice@test.com' }],
  page: 1,
  pageSize: 10,
  total: 50
};

Infer Keyword in Generics

// Extract return type of a function
type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never;

function getString(): string { return 'hello'; }
type Result = ReturnOf<typeof getString>; // string

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

type NumberArray = number[];
type Num = ElementOf<NumberArray>; // number

Recursive Generics

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

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// All nested properties are readonly