TypeScript Generics

Generics let you write functions, classes and interfaces that work with any type while preserving full type safety. They are the foundation of reusable, type-safe code.

Without generics, you would use any (losing type safety) or write duplicate code for every type.

The Problem Generics Solve

TypeScript
// ❌ Without generics — loses type information
function first(arr: any[]): any {
  return arr[0];
}
const num = first([1, 2, 3]);   // any — we lost the type!
const str = first(["a", "b"]); // any — same problem

// ✅ With generics — preserves type information
function first<T>(arr: T[]): T {
  return arr[0];
}
const num = first([1, 2, 3]);   // number — type preserved!
const str = first(["a", "b"]); // string — type preserved!

Generic Functions

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

// TypeScript infers T from the argument
identity("hello"); // T = string
identity(42);      // T = number

// Or specify explicitly
identity<string>("hello");

Multiple Type Parameters

TypeScript
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

pair("hello", 42);       // [string, number]
pair(1, true);            // [number, boolean]

Generic Interfaces

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

// A user response
const userResponse: ApiResponse<{ name: string; age: number }> = {
  data: { name: "Sita", age: 21 },
  status: 200,
  message: "OK",
};

// A list response
const listResponse: ApiResponse<string[]> = {
  data: ["Kathmandu", "Pokhara"],
  status: 200,
  message: "OK",
};

Generic Classes

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

const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
numbers.pop();  // 2 — typed as number

const strings = new Stack<string>();
strings.push("hello");
strings.push("world");

Generic Constraints

Use extends to require that a type has certain properties:

TypeScript
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(`Length: ${item.length}`);
}

logLength("hello");    // 5
logLength([1, 2, 3]);  // 3
logLength({ length: 10, name: "test" }); // 10
// logLength(42);      // Error: number doesn't have .length

The keyof Constraint

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

const user = { name: "Sita", age: 21 };
getProperty(user, "name");  // string — type-safe
getProperty(user, "age");   // number — type-safe
// getProperty(user, "email"); // Error: "email" is not in keyof User

Built-in Utility Types

TypeScript ships with powerful generic utility types:

Utility What it does Example
Partial<T> All properties optional Partial<User>
Required<T> All properties required Required<User>
Readonly<T> All properties readonly Readonly<User>
Pick<T, K> Only specified properties Pick<User, "name" | "age">
Omit<T, K> All except specified properties Omit<User, "password">
Record<K, V> Object with keys K and values V Record<string, number>
Exclude<T, U> Remove types from a union Exclude<"a" | "b", "a">
ReturnType<F> Get return type of a function ReturnType<typeof myFunc>
TypeScript
interface User {
  name: string;
  age: number;
  email: string;
  password: string;
}

type PublicUser = Omit<User, "password">;
type UserUpdate = Partial<Pick<User, "name" | "email">>;

Tips

  • Use generics, not any — generics preserve type information.
  • Name type parameters with single capital letters: T, K, V, or descriptive names like TItem, TResponse.
  • Use constraints (extends) to prevent misuse.
  • Start simple — add generic parameters only when you need them.

Next: modules — organising code across files.