TypeScript Generics

Generics let you write functions and types that work across many types while still preserving full type safety, instead of falling back to any.

Why Generics Exist

Without generics, a reusable function either has to be typed for one specific type, which limits reuse, or typed as any, which throws away all type checking. Generics solve this by letting the caller supply the type as a parameter, so the function stays reusable and fully type-checked at the same time.

A Generic Function

function firstElement<T>(items: T[]): T | undefined {
  return items[0];
}

const firstNum = firstElement([10, 20, 30]); // number | undefined
const firstStr = firstElement(["a", "b"]);   // string | undefined
console.log(firstNum, firstStr);

Generic Interfaces and Type Aliases

Generics aren't limited to functions. Interfaces and type aliases can take type parameters too, which is how you build reusable shapes like a paginated response or a key-value cache that adapts to whatever content type you plug in.

A Generic Interface

interface ApiResponse<T> {
  data: T;
  success: boolean;
}

function wrapResponse<T>(data: T): ApiResponse<T> {
  return { data, success: true };
}

const res = wrapResponse({ id: 1, name: "Aiko" });
console.log(res.data.name, res.success);
Note: By convention, generic type parameters are named T, U, K, V for simple cases, but descriptive names like TItem or TResponse are common in larger codebases.

Constraining Generics with extends

Sometimes a generic function needs to assume the type has certain properties, such as a length or an id. The extends keyword constrains a type parameter to only accept types matching a given shape, giving you access to those members inside the function.

  • T extends U restricts the type parameter to types compatible with U.
  • Constraints let you safely access properties inside the generic function body.
  • You can use multiple type parameters, each with its own independent constraint.
  • Default type parameters (T = string) let callers omit the type argument entirely.

Constrained Generic

interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id);
}

const users = [{ id: 1, name: "Ana" }, { id: 2, name: "Leo" }];
console.log(findById(users, 2));

A Realistic Use Case: A Generic Cache

Generics shine in shared utilities like a small in-memory cache, which needs to store any value type while still returning the correct type back out when a value is read.

Generic Cache Class

class Cache<T> {
  private store = new Map<string, T>();

  set(key: string, value: T): void {
    this.store.set(key, value);
  }

  get(key: string): T | undefined {
    return this.store.get(key);
  }
}

const userCache = new Cache<{ name: string }>();
userCache.set("u1", { name: "Mira" });
console.log(userCache.get("u1")?.name);
SyntaxMeaning
<T>A generic type parameter placeholder
T extends ShapeT must be compatible with Shape
T = stringDefault type argument if none is provided
Array<T> / T[]An array whose elements are all type T
Note: If you find yourself writing the same function three times for three different types, that's usually a sign it should be a generic instead.

Exercise: TypeScript Generics

Does a generic type parameter have to be named `T`?