blog

Notes on TypeScript Generics

Generics are the part of TypeScript that trips people up the most. They also unlock the most powerful patterns. Here are the parts I find useful and return to often.

The basic idea

A generic is a placeholder for a type, decided when the function or type is used — not when it is defined.

function identity<T>(value: T): T {
  return value;   // highlighted: T flows through unchanged
}                 // highlighted: return type matches input type
 
identity(42);       // T is inferred as number
identity("hello");  // T is inferred as string

The angle bracket <T> is the generic parameter. T is just a convention — you can call it anything.

Constraining generics

Without constraints, TypeScript knows very little about T. You can constrain it with extends:

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}
 
getLength("hello");  // ✓
getLength([1, 2, 3]); // ✓
getLength(42);        // ✗ Number has no .length

This is the key pattern: constrain to what you actually need, nothing more.

Generic interfaces and types

interface ApiResponse<T> {
  data: T;
  error: string | null;
  status: number;
}
 
type UserResponse = ApiResponse<User>;
type PostsResponse = ApiResponse<Post[]>;

Utility types (the ones I actually use)

TypeScript ships with generic utilities that are worth knowing:

UtilityWhat it does
Partial<T>Makes all properties optional
Required<T>Makes all properties required
Pick<T, K>Selects a subset of keys
Omit<T, K>Removes a subset of keys
Record<K, V>Maps keys to a value type
ReturnType<T>Extracts a function's return type
type User = {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
};
 
// Only the fields needed for a form
type UserForm = Pick<User, 'name' | 'email'>;
 
// Update payload — everything optional
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;

Conditional types

type IsArray<T> = T extends any[] ? true : false;
 
type A = IsArray<string[]>; // true
type B = IsArray<string>;   // false

More useful: infer lets you extract a type from within another:

type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
 
type Resolved = UnpackPromise<Promise<string>>; // string
type Plain = UnpackPromise<number>;             // number

The thing that took me longest to understand

Generic parameters are not runtime values. They exist only at the type level and are erased in the compiled output. You cannot do this:

function create<T>(): T {
  return new T(); // ✗ T is not available at runtime
}

If you need runtime type information, you have to pass the class or a factory function as a value:

function create<T>(Ctor: new () => T): T {
  return new Ctor();
}

This small mental shift — understanding the compile-time/runtime divide — makes everything else click.