Handbooks / TypeScript / Chapter 4

Generics

7 pages · ~5 min✓ Reviewed

Builds on Narrowing. Next up: Utility and Mapped Types.

TypeScriptPage 1

Generics Mental Model

1. What a Generic Is

  • A generic is a type-level parameter: function id<T>(x: T): T
  • One declaration works for many types without losing type info
anyT
Input typediscardedlinked to output
id(5) returnsanyliteral 5

2. Inference and Erasure

  • Inferred from call arguments: id('a') gives T = 'a'
  • Pass explicitly when inference fails: id<string>('a')
typescript
function id<T>(x: T): T { return x }
id(5)           // T = 5
id('a')         // T = 'a'
id<string>('a') // T = string

explicit argument overrides inference

Erased at runtime

T exists only for the compiler. So no new T() and no typeof T.

3. Interview Point

  • What do generics fix over any? T links input to output, so type info survives
  • Available at runtime? No, erased; new T() is impossible
  • Infer or explicit? Inferred from arguments; explicit when no argument fixes T
TypeScriptPage 2

Generic Functions, Interfaces and Classes

1. Declaring Generics

FormDeclareUse
Functionfunction first<T>(a: T[]): T | undefined { return a[0]; }first([1, 2])
Interfaceinterface Box<T> { value: T }Box<number>
Call signatureinterface Fn<T> { (x: T): T }Fn<string>
Aliastype Pair<A, B> = [A, B]Pair<string, number>

2. Generic Class

  • new Stack<number>() fixes T for that instance
typescript
class Stack<T> {
  private items: T[] = [];
  push(x: T) { this.items.push(x) }
  pop(): T | undefined { return this.items.pop() }
}

3. Rules

  • Arrow in .tsx: write <T,>(x: T) => x; the comma stops JSX parsing
  • Several params are comma-separated; supply each unless defaulted
Static

Static members cannot use T: it belongs to the instance side only.

4. Interview Point

  • Declare a generic class? class C<T> {}, then new C<number>()
  • Static using T? Statics live on the constructor, not on instances, so no T
  • Interface vs alias? Interface extends and merges; alias also names unions and tuples
TypeScriptPage 3

Constraints with extends

1. Constraint syntax

  • T extends { length: number }: callers may pass only types that have length
  • Bare T is unknown-like, so x.length errors
  • T keeps the specific type: len([1, 2]) gives T = number[], len('abc') gives T = string
typescript
function len<T extends { length: number }>(x: T) {
  return x.length
}

accepts string, array, any object with length

2. Dependent params and constructors

  • K extends keyof T ties the key to the object
  • T extends new (...a: any[]) => object accepts a class you can new
typescript
function pick<T, K extends keyof T>(o: T, k: K): T[K]
function make<T extends new (...a: any[]) => object>(C: T) {
  return new C()
}

3. Interview Point

  • extends in a constraint? It means assignable to, not class inheritance
  • Why no .length on bare T? T could be anything, so only unknown members exist
  • One param depending on another? K extends keyof T
Floor, not replacement

The constraint only checks the argument. T is still inferred from it.

TypeScriptPage 4

Defaults, keyof and Indexed Access

1. Default type parameters

  • interface Res<T = unknown> { data: T }, so Res means Res<unknown>
  • Applied only when T is not given and not inferred
  • Defaulted params go after required ones
  • Default must satisfy the constraint: <T extends object = {}>

2. keyof and indexed access

TypeGives
keyof {a: 1; b: 2}'a' | 'b'
keyof any[]number + method names
keyof {[k: string]: X}string | number
User['id']type of property id
T[number]array element type
T[keyof T]union of all value types

3. Typed getter

  • K extends keyof T: wrong keys fail at compile time
  • T[K] makes the return type exact, not a union
typescript
function get<T, K extends keyof T>(o: T, k: K): T[K] { return o[k] }
get({ id: 1 }, 'id')  // number
get({ id: 1 }, 'x')   // error
TypeScriptPage 5

Common Generic Mistakes

1. Smells in the signature

  • function f<T>(x: T): void: T used once relates nothing, so it is a smell
  • Each T should link two positions, such as param and return
  • as T or {} as T hides errors; the compiler cannot verify it

2. Constraint traps

MistakeWhy it failsFix
T extends string, return 'a'T may be a narrower literalReturn string
typeof x === 'string'Does not narrow T itselfOverloads or conditional types

3. Untyped keys

  • Key typed string: implicit any on o[k], safety lost
  • Use K extends keyof T instead
typescript
function get<T, K extends keyof T>(
  o: T, k: K
): T[K] {
  return o[k];
}
Remember

The caller picks T, so you cannot return a concrete value for it. To debug inference, hover the call or pass <T> explicitly.

TypeScriptPage 6

Generics Cheat Sheet

1. Declare

FormSyntax
Functionfunction f<T>(x: T): T
Interfaceinterface I<T> { v: T }
Classclass C<T> {}
Aliastype A<T> = T[]

2. Constrain and index

WriteMeans
<T extends U>constrain T
<K extends keyof T>K depends on T
<T = string>default, last
keyof Tkey union
T[K]value type
T[number]array element
T[keyof T]all values

3. The idiom

typescript
function get<T, K extends keyof T>(o: T, k: K): T[K] {
  return o[k];
}

type-safe property access

4. Interview Point

  • Type-safe pick? pick<T, K extends keyof T>(o: T, ks: K[]): Pick<T, K>
  • Ties key to object? K extends keyof T
  • Default order? Defaults go last, after required params
TypeScriptPage 7

Check yourself

function len<T>(x: T) { return x.length } fails to compile. Why, and what is the fix?
  • A bare T could be anything, so the compiler knows no properties on it
  • Constrain it: <T extends { length: number }>
Given get<T, K extends keyof T>(o: T, k: K): T[K], what is the type of get({a: 1, b: 'x'}, 'b')? What about get({a: 1}, 'c')?
  • string, because K is inferred as 'b' and T['b'] is string
  • The second call is a compile error: 'c' is not in keyof T
function f<T extends string>(): T { return 'a' } errors. Why?
  • The caller may choose T = 'b', and 'a' is not assignable to 'b'
  • T is the caller's type, so you cannot return a concrete value for it