TypeScriptType-Level Programming

Type-Level Programming

TypeScript's type system is Turing-complete. Beyond describing the shapes of values, you can write programs that run entirely at compile time: computing new types from existing types using conditional types, mapped types, template literal types, and recursive type operations.

This page walks through the key primitives and combines them into real-world patterns.

The Building Blocks

Type-level programming relies on four core features:

  1. Conditional typesT extends U ? A : B
  2. Mapped types{ [K in keyof T]: ... }
  3. Template literal types\prefix_${T}``
  4. Infer — extracting types from patterns

Let us look at each in depth.

Conditional Types

Conditional types are the if/else of the type system:

TS
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<"hello">;  // true (string literal extends string)

Conditional types distribute over union types by default when T is a bare type parameter:

TS
type ToArray<T> = T extends unknown ? T[] : never;

// Distribution: each member of the union is processed separately
type Result = ToArray<string | number>;
// Equivalent to: string[] | number[]
// NOT (string | number)[]

// To prevent distribution, wrap in a tuple
type ToArrayNoDistrib<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNoDistrib<string | number>;  // (string | number)[]
Note
Distribution only happens when T is a naked type parameter — not when it is wrapped in a tuple, object, or another generic.
The infer Keyword

infer lets you extract sub-types from a pattern during a conditional type check. Think of it as pattern matching for types:

TS
// Extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R  = ReturnType<Fn>;  // string

// Extract the element type of a Promise
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;

type P = Awaited<Promise<Promise<number>>>;  // number

// Extract parameters as a tuple
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type Params = Parameters<(a: string, b: number) => void>;  // [string, number]

TS
// Extract the first element of a tuple
type Head<T extends readonly unknown[]> =
  T extends readonly [infer First, ...unknown[]] ? First : never;

type H = Head<[string, number, boolean]>;  // string

// Extract everything except the first element
type Tail<T extends readonly unknown[]> =
  T extends readonly [unknown, ...infer Rest] ? Rest : never;

type T2 = Tail<[string, number, boolean]>;  // [number, boolean]
Mapped Types

Mapped types transform every property in an object type. They are the Array.map of object types:

TS
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Partial<T> = {
  [K in keyof T]?: T[K];
};

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser   = Partial<User>;    // all optional
type NullableUser  = Nullable<User>;   // all nullable
type ReadonlyUser  = Readonly<User>;   // all readonly

You can also remap keys with as and filter properties:

TS
// Add "get" prefix to every key
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User { id: number; name: string; }

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; }

// Filter: keep only string-valued properties
type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed { id: number; name: string; active: boolean; role: string; }
type OnlyStrings = StringProps<Mixed>;
// { name: string; role: string; }
Template Literal Types

Template literal types let you construct string literal types programmatically:

TS
type EventName<T extends string> = `on${Capitalize<T>}`;

type MouseEvents = EventName<'click' | 'hover' | 'focus'>;
// 'onClick' | 'onHover' | 'onFocus'

// Build CSS property names
type CSSPropertyX = `margin-${string}` | `padding-${string}`;

// Extract the event name from a handler key
type StripOn<T extends string> = T extends `on${infer Event}` ? Uncapitalize<Event> : never;

type Events = StripOn<'onClick' | 'onHover'>;  // 'click' | 'hover'
Recursive Types

Type-level programs can recurse, enabling deep transformations:

TS
// Deep readonly — makes every nested object readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
  server: { host: string; port: number; };
  db:     { url: string; poolSize: number; };
}

type FrozenConfig = DeepReadonly<Config>;
// server.host becomes readonly — even nested!

// Deep partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type PartialConfig = DeepPartial<Config>;
// { server?: { host?: string; port?: number } }

TS
// Flatten a nested array type
type Flatten<T> = T extends Array<infer Item>
  ? Item extends Array<unknown>
    ? Flatten<Item>
    : Item
  : T;

type Nested   = Flatten<number[][][]>;  // number
type Shallow  = Flatten<string[]>;      // string
type NotArray = Flatten<boolean>;       // boolean
Warning
TypeScript limits recursion depth. If you hit "Type instantiation is excessively deep", restructure the recursion or use tail-recursive patterns with accumulators.
Building a Type-Safe API Client

Here is a real-world example that combines all the primitives to derive a typed HTTP client from a route definition:

TS
interface Routes {
  'GET /users':             { response: User[] };
  'GET /users/:id':         { params: { id: string }; response: User };
  'POST /users':            { body: CreateUserDto; response: User };
  'DELETE /users/:id':      { params: { id: string }; response: void };
}

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

// Extract all routes for a given method
type RoutesForMethod<M extends Method> = {
  [K in keyof Routes as K extends `${M} ${string}` ? K : never]: Routes[K];
};

type GetRoutes  = RoutesForMethod<'GET'>;
// { 'GET /users': {...}; 'GET /users/:id': {...} }

// Extract the response type from a route key
type ResponseOf<K extends keyof Routes> =
  Routes[K] extends { response: infer R } ? R : never;

type UsersResponse = ResponseOf<'GET /users'>;  // User[]
Type-Safe Event Emitter

TS
type EventMap = {
  login:   { userId: string; timestamp: Date };
  logout:  { userId: string };
  message: { from: string; text: string; roomId: number };
};

type EventKey = keyof EventMap;

class TypedEmitter<Events extends Record<string, unknown>> {
  private listeners: Partial<{
    [K in keyof Events]: Array<(data: Events[K]) => void>;
  }> = {};

  on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): void {
    const arr = (this.listeners[event] ??= []) as Array<(data: Events[K]) => void>;
    arr.push(handler);
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.listeners[event]?.forEach(h => h(data));
  }
}

const emitter = new TypedEmitter<EventMap>();

emitter.on('login', ({ userId, timestamp }) => {
  console.log(`${userId} logged in at ${timestamp}`);
});

emitter.emit('login', { userId: 'u-1', timestamp: new Date() });  // OK
emitter.emit('login', { userId: 'u-1' });                          // Error: missing timestamp
Utility Types from Scratch

Implementing built-in utilities from scratch is the best way to understand type-level programming:

TS
// Exclude<T, U> — remove members of U from T
type MyExclude<T, U> = T extends U ? never : T;

type Nums = MyExclude<string | number | boolean, string>;  // number | boolean

// Extract<T, U> — keep only members of T that extend U
type MyExtract<T, U> = T extends U ? T : never;

type Strs = MyExtract<string | number | boolean, string | boolean>;  // string | boolean

// NonNullable<T>
type MyNonNullable<T> = T extends null | undefined ? never : T;

// Required<T>
type MyRequired<T> = {
  [K in keyof T]-?: T[K];  // -? removes the optional modifier
};

// Mutable<T> — opposite of Readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];  // -readonly removes the readonly modifier
};
Higher-Kinded Types Simulation

TypeScript does not have first-class higher-kinded types (type constructors as arguments), but you can simulate them with an interface-based encoding:

TS
// Register type constructors in a "URI" map
interface URItoKind<A> {
  'Array':   Array<A>;
  'Set':     Set<A>;
  'Promise': Promise<A>;
}

type URIS = keyof URItoKind<unknown>;

// HKT<F, A> = apply the type constructor F to argument A
type HKT<F extends URIS, A> = URItoKind<A>[F];

// A generic functor interface
interface Functor<F extends URIS> {
  map<A, B>(fa: HKT<F, A>, f: (a: A) => B): HKT<F, B>;
}

// Implement for Array
const ArrayFunctor: Functor<'Array'> = {
  map: (arr, f) => arr.map(f),
};

// Implement for Set
const SetFunctor: Functor<'Set'> = {
  map: (set, f) => new Set([...set].map(f)),
};
Tip
This HKT simulation is used by libraries like fp-ts to implement generic functional abstractions over different container types.
Common Patterns Reference

Pattern

Syntax

Use case

Conditional

T extends U ? A : B

Branch on type relationships

Infer

infer R inside extends

Extract sub-types from patterns

Mapped

{ [K in keyof T]: ... }

Transform all properties of a type

Key remapping

[K in keyof T as NewKey]

Rename or filter properties

Template literal

prefix_${T}

Construct string literal types

Recursive

type F<T> = T extends ... ? F<...> : ...

Deep transformations

Distribution

bare T in conditional

Apply to each union member separately

No distribution

[T] extends [...]

Treat union as a whole

Key Takeaways
  1. TypeScript's type system is Turing-complete — types can compute types

  2. Conditional types (T extends U ? A : B) are the if/else of the type level

  3. infer extracts sub-types from patterns, like regex capture groups for types

  4. Mapped types transform every property — add readonly, remove optional, rename keys

  5. Template literal types build string literal types from existing types

  6. Recursive types enable deep transformations like DeepReadonly and DeepPartial

  7. Conditional types distribute over unions when T is a naked type parameter

  8. Use [T] extends [...] to prevent distribution and treat the union as a whole