intermediate Step 10 of 18

Generics

TypeScript Programming

Generics

Generics allow you to write functions, classes, and interfaces that work with any type while preserving type safety. Instead of using any (which throws away type information) or writing separate functions for each type, generics let you parameterize types. The actual type is determined when the function is called or the class is instantiated, and TypeScript tracks it throughout the scope. Generics are one of TypeScript's most important features for building reusable libraries and frameworks.

Generic Functions

A generic function declares one or more type parameters in angle brackets before the parameter list. The type parameter acts as a placeholder that gets filled in when the function is called.

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

// TypeScript infers T from the argument
const str = identity("hello");    // T is string, returns string
const num = identity(42);          // T is number, returns number
const arr = identity([1, 2, 3]);   // T is number[], returns number[]

// Explicit type argument
const explicit = identity<boolean>(true);

// Generic function with multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const result = pair("age", 30);    // [string, number]
const coords = pair(10.5, 20.3);   // [number, number]

Generic Interfaces and Type Aliases

Generics work with interfaces and type aliases to create reusable type templates for data structures.

// Generic interface for API responses
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface User {
  id: number;
  name: string;
}

// Specialize the generic with a concrete type
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "OK",
  timestamp: new Date().toISOString(),
};

const listResponse: ApiResponse<User[]> = {
  data: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
  status: 200,
  message: "OK",
  timestamp: new Date().toISOString(),
};

Generic Classes

Classes can also be parameterized with generics. This is commonly used for data structures like stacks, queues, and repositories.

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(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20 (type: number | undefined)

const stringStack = new Stack<string>();
stringStack.push("hello");
// stringStack.push(42); // Error: number is not assignable to string

Generic Constraints

Constraints limit the types that can be used as generic arguments using the extends keyword. This ensures the generic type has certain properties or capabilities.

// Constraint: 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(42);       // Error — number has no length

// Constraint: key must be a key of the object
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" };
const name = getProperty(person, "name");   // type: string
const age = getProperty(person, "age");     // type: number
// getProperty(person, "phone");            // Error: "phone" is not a key of person

// Multiple constraints with intersection
interface HasId { id: number; }
interface HasName { name: string; }

function display<T extends HasId & HasName>(item: T): string {
  return `#${item.id}: ${item.name}`;
}
Tip: Name generic type parameters with descriptive single letters by convention: T for a general type, K for keys, V for values, E for elements. For complex generics with multiple parameters, consider longer names like TInput and TOutput.

Key Takeaways

  • Generics parameterize types, enabling reusable code that preserves type safety.
  • TypeScript can infer generic type arguments from function call arguments.
  • Generic interfaces and classes create flexible, reusable data structure templates.
  • Use extends to constrain generic types to those with required properties.
  • The keyof operator combined with generics enables type-safe property access.