Generics

intermediate
13 min

Generics

Generics let you write code that works over many types while preserving full type safety. Instead of choosing between duplicating a function for every type or falling back to any, you write it once with a type parameter that gets filled in at each call site. Generics power almost every library type you use — Array<T>, Promise<T>, Map<K, V> — so learning to read and write them is a major milestone.

The Problem Generics Solve

Consider a function that returns the first element of an array:

typescript
function firstAny(arr: any[]): any { return arr[0]; } const n = firstAny([1, 2, 3]); // n is any — type information lost

With a generic type parameter, the relationship between input and output is preserved:

typescript
function first<T>(arr: T[]): T | undefined { return arr[0]; } const a = first([1, 2, 3]); // a: number | undefined const b = first(["x", "y"]); // b: string | undefined

T is a placeholder. TypeScript infers it from the argument, so you almost never need to write first<number>([1, 2, 3]) explicitly.

Generic Constraints with extends

Sometimes your function needs the type parameter to have certain capabilities. Constraints express that:

typescript
function longest<T extends { length: number }>(a: T, b: T): T { return a.length >= b.length ? a : b; } longest("hello", "hi"); // OK: strings have length longest([1, 2], [3]); // OK: arrays have length longest(10, 20); // Error: number has no length

A particularly useful combination pairs a constraint with keyof to type property access safely:

typescript
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, name: "Ada" }; pluck(user, "name"); // string pluck(user, "email"); // Error: "email" is not a key of user

Generic Interfaces and Type Aliases

Types themselves can be parameterized. This is how you model reusable containers like API responses:

typescript
interface ApiResponse<T> { success: boolean; data: T; error?: string; } type UserResponse = ApiResponse<{ id: number; name: string }>; type ListResponse = ApiResponse<string[]>;

Defaults are allowed too: interface Box<T = string> { value: T }.

Generic Classes

typescript
class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } get size(): number { return this.items.length; } } const numbers = new Stack<number>(); numbers.push(1); numbers.push("two"); // Error: string is not assignable to number

One Stack implementation, safely reusable for any element type.

Generics in Async Code

Promise<T> is a generic you use daily. Typing a fetch helper makes the whole call chain safe:

typescript
async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); return res.json() as Promise<T>; } const todos = await getJson<{ id: number; title: string }[]>("/api/todos"); todos[0].title; // fully typed

Practical Guidelines

  • Introduce a type parameter only when it appears at least twice in the signature (linking input to output or two inputs together). A parameter used once is usually noise.
  • Name simple parameters T, K, V; use descriptive names like TItem or TResponse when there are several.
  • Push constraints as far as possible with extends so implementations can actually use the values they receive.

Key Takeaways

  • Generics preserve type relationships that any destroys.
  • Type arguments are usually inferred from usage — explicit <T> at call sites is rare.
  • extends constrains what a type parameter must support; K extends keyof T types property access.
  • Interfaces, type aliases, and classes can all be generic, with optional defaults.

Next lesson: Utility TypesPartial, Pick, Omit, Record, and friends built on generics.

Generics - TypeScript | CodeYourCraft | CodeYourCraft