frontend / typescript / 01_generics.md

TypeScript Generics

5 min read source

TypeScript Generics

TL;DR

Generics are type-level parameters. They let a function, class, or type be reused across many concrete types without losing type information (the alternative is any, which loses everything). The senior moves: constraining generics with extends, using keyof + lookup types to make APIs type-safe against an object’s actual shape, and knowing when not to add a generic.

Interview Q&A

Q: What’s a generic, and why use one over any?

A generic introduces a type variable that’s bound at the call site. The type relationship between input and output is preserved; with any it’s erased.

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

const n = identity(42)        // n: number
const s = identity('hello')   // s: string

// vs any:
function identityAny(value: any): any { return value }
const x = identityAny(42)     // x: any — error-prone

The contract “what comes out has the same type as what went in” is encoded.

Q: Generic function vs generic interface vs generic class — when each?

// Generic function: each call binds T fresh.
function first<T>(arr: T[]): T | undefined { return arr[0] }

// Generic interface: T is bound when the interface is used.
interface Box<T> { value: T }
const a: Box<number> = { value: 1 }

// Generic class: T is bound at instantiation.
class Stack<T> {
  private items: T[] = []
  push(item: T) { this.items.push(item) }
  pop(): T | undefined { return this.items.pop() }
}
const s = new Stack<string>()

Rule of thumb: use a generic function when the type flows through one call; a generic interface/type to describe a parameterized shape; a generic class when state with that shape is held over time.

Q: How do generic constraints (extends) work?

T extends X means “T must be assignable to X.” You get to use X’s members on T inside the function.

function getLength<T extends { length: number }>(x: T): number {
  return x.length  // allowed — T provably has .length
}

getLength('hello')   // ok
getLength([1, 2, 3]) // ok
getLength(42)        // error — number has no .length

Constraints narrow what T can be and unlock its members. Without the constraint, x.length would error.

Q: What are default type parameters good for?

A default lets a generic be omitted at the call site and still be useful — common for “container” types and React component generics.

interface ApiResponse<T = unknown> {
  data: T
  status: number
}

const r1: ApiResponse = { data: 'something', status: 200 }       // T = unknown
const r2: ApiResponse<User> = { data: { id: 1 }, status: 200 }   // T = User

Defaults also avoid breaking callers when you add a generic later.

Q: What does keyof do, and how do you use it with lookup types?

keyof T is the union of the property names of T. T[K] is the type at key K. Combined, they give you a type-safe “get any property” API:

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

const user = { id: 1, name: 'Ada', email: 'a@b.com' }
const id = pluck(user, 'id')       // id: number
const name = pluck(user, 'name')   // name: string
pluck(user, 'nope')                // error — 'nope' not in keyof typeof user

This is the single most common generic pattern in real code (form libraries, ORMs, validators).

Q: How do you type a function that maps an object’s values?

function mapValues<T extends object, U>(
  obj: T,
  fn: <K extends keyof T>(value: T[K], key: K) => U,
): Record<keyof T, U> {
  const out = {} as Record<keyof T, U>
  for (const k in obj) out[k] = fn(obj[k] as T[Extract<keyof T, string>], k)
  return out
}

The generic K inside fn lets the callback know exactly which key’s value it received.

Q: How do generic constraints interact with inference?

TS infers from arguments. If you constrain too loosely, you lose information; too tightly, you over-restrict callers.

// Too loose: T inferred as { id: number } only — loses the rest of the object.
function withId<T extends { id: number }>(x: { id: number }): T { return x as T }

// Right: T preserved fully through the function.
function withId2<T extends { id: number }>(x: T): T { return x }

const u = withId2({ id: 1, name: 'Ada' })  // u: { id: number; name: string }

The shape “argument is T, return is T” is what preserves narrow inference.

Q: When shouldn’t you reach for generics?

When the generic is never actually used as a type relationship. If T appears only once in the signature, it’s almost certainly wrong — you wanted a constraint, not a generic.

// Smell — T appears only in return position. Caller must specify it.
function parse<T>(json: string): T { return JSON.parse(json) }
const u = parse<User>('{}')  // looks safe; isn't. T is unverified.

// Better:
function parse(json: string): unknown { return JSON.parse(json) }
// caller validates with zod/io-ts/etc and gets a typed value

A generic that appears only on the return is just a polite any.

Gotchas / edge cases

  • Arrow generics in .tsx<T>(x: T) => x looks like JSX. Use <T,>(x: T) => x (trailing comma) or <T extends unknown>(x: T) => x.
  • Over-constrainingT extends string when you really meant “any value”; the constraint leaks into the public API.
  • Object vs {} vs object vs Record<string, unknown> — all different. Prefer Record<string, unknown> or a specific shape.
  • Inference can collapse to a base type — passing two different object types into a function generic on T may infer T as their union, not what you wanted; use a constraint to anchor it.
  • Generic defaults don’t fix everything — a default of unknown still requires narrowing before use.
  • Higher-kinded types don’t exist in TS — you can’t write F<T> where F is itself a generic parameter (no Functor<F>). You work around with helper types.

What a senior is expected to say

A junior says “generics make code reusable.” A senior says: generics encode relationships between types (input shape ↔ output shape, key ↔ value type, request ↔ response shape). Constraints (extends) plus keyof T + lookup types T[K] are the workhorse pattern in real codebases — form libraries, query builders, ORMs are all built on it. The senior also flags the anti-pattern: a generic that only appears in the return position is a polite any masquerading as type safety.

Cross-references

Further reading