TypeScript

Generics

Write reusable, type-safe code with TypeScript generics — one of the most powerful features of the type system.

Introduction to Generics

Generics allow you to write reusable code that works with multiple types while maintaining type safety. Think of them as type parameters.

// Without generics - loses type information
function identity(arg: any): any {
  return arg;
}

const result = identity("hello"); // type: any (lost!)

// With generics - preserves type information
function identity<T>(arg: T): T {
  return arg;
}

const str = identity<string>("hello"); // type: string
const num = identity<number>(42);      // type: number
const inferred = identity("world");    // TypeScript infers T = string

// Generic array function
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const first = firstElement([1, 2, 3]); // type: number
const name = firstElement(["Alice", "Bob"]); // type: string

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

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

Generic Constraints

Constraints limit what types can be passed to a generic, allowing you to access specific properties safely.

// Constraint: T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("alice", "bob");          // string
longest([1, 2, 3], [4, 5]);       // number[]
longest({ length: 3 }, { length: 5, value: "x" }); // object
// longest(1, 2); // Error: number doesn't have length

// Constraint: K must be a key of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 25, active: true };
const userName = getProperty(user, "name");   // type: string
const userAge = getProperty(user, "age");     // type: number
// getProperty(user, "invalid"); // Error: not a valid key

// Conditional constraint
type NonNullableType<T> = T extends null | undefined ? never : T;

type A = NonNullableType<string | null>;     // string
type B = NonNullableType<number | undefined>; // number
type C = NonNullableType<null>;               // never

Generic Classes & Interfaces

Classes and interfaces can also be generic, enabling powerful data structure implementations.

// Generic Stack data structure
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;
  }

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

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

// Generic Result type (like Rust's Result)
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return { ok: false, error: "Cannot divide by zero" };
  return { ok: true, value: a / b };
}

const result = divide(10, 2);
if (result.ok) {
  console.log(result.value); // type: number
} else {
  console.error(result.error); // type: string
}

Generics are the foundation of type-safe utility libraries. Understanding them unlocks the full power of TypeScript.