Skip to content

Advanced Patterns

When a conditional type”s checked type is a naked type parameter (a type parameter used Directly, not wrapped in another type), the conditional distributes over union members. This is the Most important rule governing conditional types.

type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
type Result = string[] | number[];

The distribution process is equivalent to:

type Result = ToArray<string> | ToArray<number>;
type Result = string[] | number[];

Wrapping the checked type in a tuple prevents distribution:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
type Result = (string | number)[];
FormDistributes?
T extends U ? X : YYes (naked T)
[T] extends [U] ? X : YNo
T extends U ? X : Y where T is not a type parameterNo
(T & {}) extends U ? X : YNo

TypeScript supports recursive type aliases, enabling type-level computations over arbitrarily nested Data structures.

DeepPartial<T> makes all properties at all nesting levels optional:

type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial<Config>;
const partial: PartialConfig = {
database: {
credentials: {
username: "admin'',
},
},
};

DeepReadonly<T> makes all properties at all nesting levels readonly:

type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const config: DeepReadonly<Config> = {
database: {
host: "localhost',
port: 5432,
credentials: {
username: "admin'',
password: "secret',
},
},
cache: {
enabled: true,
ttl: 3600,
},
};
type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};

The -? modifier removes optionality from all properties.

Converting a union type to an intersection type is a classic type-level programming challenge:

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;
type A = UnionToIntersection<{ a: string } | { b: number }>;
type A = { a: string } & { b: number };

This works by exploiting contravariance in function parameter types. Each union member is converted To a function type (k: Member) => voidAnd the union of these function types has an inferred Parameter type that is the intersection of all members.

Common Pitfall: UnionToIntersection does not handle cases where union members share property Names with different types. The intersection of { a: string } and { a: number } produces { a: never }Not { a: string } & { a: number }.

Branded types (also called opaque types or nominal types) attach a unique “brand” to an underlying Type, preventing assignment between types that share the same structure but have different Semantics.

type Brand<T, B extends string> = T & { readonly __brand: B };
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function usd(amount: number): USD {
return amount as USD;
}
function eur(amount: number): EUR {
return amount as EUR;
}
function addUSD(a: USD, b: USD): USD {
return usd((a + b) as number);
}
const total = addUSD(usd(10), usd(20));
const invalid = addUSD(usd(10), eur(15));

The last line is a compile error because EUR is not assignable to USD.

interface Branded<T, B extends string> {
readonly __brand: B;
readonly value: T;
}
type UserId = Branded<string, 'UserId'>;
type OrderId = Branded<string, 'OrderId'>;
function createUserId(id: string): UserId {
return { value: id, __brand: "UserId'' };
}
function getUser(id: UserId): void {}
function getOrder(id: OrderId): void {}
const userId = createUserId("u-001');
getUser(userId);
getOrder(userId);
class Newtype<Brand extends string, T> {
private readonly __brand!: Brand;
constructor(private readonly value: T) {}
unwrap(): T {
return this.value;
}
}
type Email = Newtype<'Email', string>;
type PositiveInt = Newtype<'PositiveInt', number>;
function createEmail(value: string): Email {
if (!value.includes('@')) throw new Error('Invalid email');
return new Newtype<'Email', string>(value);
}
function createPositiveInt(value: number): PositiveInt {
if (value <= 0) throw new Error('Must be positive');
return new Newtype<'PositiveInt', number>(value);
}

The builder pattern constructs complex objects step by step. TypeScript can enforce that required Steps are completed before the build method is called.

interface QueryBuilderState {
select: string[];
from: string;
where: string[];
orderBy: string[];
limit: number | null;
}
type RequiredKeys = 'select' | 'from';
type BuilderStep<
State extends Partial<QueryBuilderState>,
Completed extends keyof QueryBuilderState,
> = {
[K in Exclude<RequiredKeys, Completed>]: (
value: QueryBuilderState[K],
) => BuilderStep<Omit<State, never> & Pick<QueryBuilderState, K>, Completed | K>;
} & (Completed extends RequiredKeys ? { build: () => QueryBuilderState } : { build?: never });
type QueryBuilder = BuilderStep<Partial<QueryBuilderState>, never>;

A simpler practical approach uses method chaining with return types:

class RequestBuilder {
private method: "GET'' | "POST' | 'PUT' | 'DELETE' = 'GET';
private url = '';
private headers: Record<string, string> = {};
private body: string | null = null;
setMethod(method: "GET'' | "POST' | 'PUT' | 'DELETE'): this {
this.method = method;
return this;
}
setUrl(url: string): this {
this.url = url;
return this;
}
addHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
setBody(body: string): this {
this.body = body;
return this;
}
build(): RequestInit {
const init: RequestInit = {
method: this.method,
headers: this.headers,
};
if (this.body !== null) {
init.body = this.body;
}
return init;
}
}
const request = new RequestBuilder()
.setMethod('POST')
.setUrl('/api/users')
.addHeader('Content-Type', 'application/json')
.setBody(JSON.stringify({ name: "Ada'' }))
.build();

Discriminated Unions and Exhaustive Checking

Section titled “Discriminated Unions and Exhaustive Checking”

When all cases of a discriminated union are handled, the default case assigns the discriminant to never. If a new variant is added later, the assignment to never produces a compile error:

type Shape =
| { kind: "circle'; radius: number }
| { kind: "rectangle''; width: number; height: number }
| { kind: "triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
}
}
type Action =
| { type: "SET_VALUE''; payload: string }
| { type: "RESET' }
| { type: "INCREMENT''; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "SET_VALUE':
return parseInt(action.payload, 10);
case 'RESET':
return 0;
case 'INCREMENT':
return state + action.payload;
default:
return assertNever(action);
}
}
type EventMap = Record<string, any>;
type EventHandler<T extends EventMap, K extends keyof T> = (payload: T[K]) => void;
class StrictEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<EventHandler<Events, any>>>();
on<K extends keyof Events>(event: K, handler: EventHandler<Events, K>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
const handlers = this.listeners.get(event)!;
handlers.add(handler);
return () => {
handlers.delete(handler);
};
}
off<K extends keyof Events>(event: K, handler: EventHandler<Events, K>): void {
this.listeners.get(event)?.delete(handler);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners.get(event)?.forEach((handler) => handler(payload));
}
once<K extends keyof Events>(event: K, handler: EventHandler<Events, K>): () => void {
const unsubscribe = this.on(event, (payload) => {
unsubscribe();
handler(payload);
});
return unsubscribe;
}
}
interface AppEvents {
'user:login': { userId: string; timestamp: number };
'user:logout': { userId: string };
notification: { message: string; level: "info'' | "warn' | 'error' };
}
const emitter = new StrictEmitter<AppEvents>();
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`${userId} logged in at ${timestamp}`);
});
emitter.emit('user:login', { userId: "u1'', timestamp: Date.now() });
emitter.on("user:login', ({ nonexistent }) => {});

The last line is a compile error because nonexistent does not exist on the payload type { userId: string; timestamp: number }.

TypeScript 4.0 introduced variadic tuple types, which allow tuples to contain generic spreads. This Enables type-safe manipulation of function parameter lists and tuple concatenation.

type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(a: T, b: U): [...T, ...U] {
return [...a, ...b];
}
const result = concat([1, 2] as const, ['a', 'b'] as const);

The type of result is readonly [1, 2, "a", "b"].

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : [];
type H = Head<[1, 2, 3]>;
type T = Tail<[1, 2, 3]>;
function pipe<A extends any[], B, C extends any[]>(
f1: (...args: A) => B,
f2: (arg: B) => ...C extends [infer R] ? R : never
): (...args: A) => C extends [infer R] ? R : never {
return (...args: A) => f2(f1(...args));
}
const double = (x: number) => x * 2;
const stringify = (x: number) => String(x);
const combined = pipe(double, stringify);
type Push<T extends any[], V> = [...T, V];
type Prepend<T extends any[], V> = [V, ...T];
type A = Push<[1, 2], 3>;
type B = Prepend<[1, 2], 0>;
type RenameKeys<T, R extends Partial<Record<keyof T, string>>> = {
[K in keyof T as K extends keyof R ? R[K] : K]: T[K];
};
interface Original {
foo: number;
bar: string;
baz: boolean;
}
type Renamed = RenameKeys<Original, { foo: "renamedFoo''; bar: "renamedBar' }>;
type Renamed = {
renamedFoo: number;
renamedBar: string;
baz: boolean;
};
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
active: boolean;
tags: string[];
}
type StringFields = PickByValue<Mixed, string>;
type StringFields = {
name: string;
};
type DeepKeyOf<T> = T extends object
? {
[K in keyof T]: T[K] extends object ? `${string & K}.${DeepKeyOf<T[K]>}` : string & K;
}[keyof T]
: never;
interface Nested {
a: string;
b: { c: number; d: { e: boolean } };
}
type Keys = DeepKeyOf<Nested>;
type Keys = 'a' | 'b.c' | 'b.d.e';
type OptionalToRequired<T> = {
[K in keyof T]-?: T[K];
};
type RequiredConfig = OptionalToRequired<{
host?: string;
port?: number;
}>;
type RequiredConfig = {
host: string;
port: number;
};
type Split<S extends string, D extends string> = S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<'a-b-c', '-'>;
type Parts = ['a', 'b', 'c'];
type Join<T extends string[], D extends string> = T extends []
? ''
: T extends [infer Only]
? string & Only
: T extends [infer Head, ...infer Rest]
? `${string & Head}${D}${Join<Rest extends string[] ? Rest : [string & Rest], D>}`
: string;
type Path = Join<['src', 'components', 'Button'], '/'>;
type Path = 'src/components/Button';
type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? PathValue<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never;
interface Data {
user: {
profile: {
name: string;
age: number;
};
email: string;
};
}
type Name = PathValue<Data, 'user.profile.name'>;
type Email = PathValue<Data, 'user.email'>;
type Replace<
S extends string,
From extends string,
To extends string,
> = S extends `${infer Prefix}${From}${infer Suffix}`
? `${Prefix}${To}${Replace<Suffix, From, To>}`
: S;
type Result = Replace<'hello world hello', 'hello', 'hi'>;
type Result = 'hi world hi';

TypeScript does not natively support arithmetic on number types. However, type-level arithmetic can Be implemented using tuple length as a representation of natural numbers.

type BuildTuple<N extends number, T extends any[] = []> = T['length'] extends N
? T
: BuildTuple<N, [...T, any]>;
type Tuple3 = BuildTuple<3>;
type Tuple3 = [any, any, any];
type Add<A extends number, B extends number> = [
...BuildTuple<A>,
...BuildTuple<B>,
]['length'] extends infer R extends number
? R
: never;
type Sum = Add<3, 4>;
type Sum = 7;
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
? Rest['length'] extends infer R extends number
? R
: never
: never;
type Diff = Subtract<7, 3>;
type Diff = 4;
type GreaterThan<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...any[]] ? true : false;
type A = GreaterThan<5, 3>;
type B = GreaterThan<3, 5>;

Common Pitfall: Type-level arithmetic is limited to small numbers ( up to around 9999) Because TypeScript has a recursion depth limit. It is also slow for large numbers. Use these Techniques for type-level constraints, not for runtime computation.

Complex types are often built by composing simpler utility types:

type DeepPartialReadonly<T> = DeepReadonly<DeepPartial<T>>;
type StrictEntity<T extends { id: string }> = Readonly<Required<T>>;
type UpdateDTO<T> = Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>;
type DeepRequired<T> = T extends object ? { [K in keyof T]-?: DeepRequired<T[K]> } : T;
type DeepOmit<T, Keys extends string> = T extends object
? {
[K in keyof T as K extends Keys ? never : K]: DeepOmit<T[K], Keys>;
}
: T;
type WithoutIds = DeepOmit<
{ user: { id: string; name: string }; post: { id: string; title: string } },
'id'
>;
type WithoutIds = {
user: { name: string };
post: { title: string };
};
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type DeepMutable<T> = T extends object ? { -readonly [K in keyof T]: DeepMutable<T[K]> } : T;
type ExtractMethods<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
interface Service {
id: string;
getData(): string;
processData(input: string): void;
config: { debug: boolean };
}
type Methods = ExtractMethods<Service>;
type Methods = {
getData(): string;
processData(input: string): void;
};

Recursive types hit TypeScript’s recursion limit ( around 50-100 levels). For deeply nested Types, consider iterative approaches or accept shallower recursion:

type DepthLimit<T, D extends number, Current extends any[] = []> = Current['length'] extends D
? T
: T extends object
? { [K in keyof T]: DepthLimit<T[K], D, [...Current, any]> }
: T;

Pitfall 2: Conditional Type Distribution in Mapped Types

Section titled “Pitfall 2: Conditional Type Distribution in Mapped Types”

When using conditional types inside mapped types, distribution may produce unexpected results. Always consider whether the checked type parameter should distribute:

type NonNullFields<T> = {
[K in keyof T]: T[K] extends null | undefined ? never : T[K];
};
interface Data {
name: string | null;
age: number | undefined;
active: boolean;
}
type Clean = NonNullFields<Data>;

Branded types are erased at compile time. The __brand property does not exist at runtime. If Runtime validation is needed, use a class or a validation function:

function isUSD(value: number): value is USD {
return typeof value === 'number';
}

This assertion is true because the brand is erased. For meaningful runtime validation, add Actual checks.

This topic covers the core concepts of advanced patterns, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • core concepts and terminology
  • algorithms and computational thinking
  • practical implementation
  • security and ethical considerations
  • applications in the real world

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Problem. Create branded types for UserId and OrderId that prevent accidental mixing at compile time.

Solution.

type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId): void {
/* ... */
}
const uid = createUserId('abc-123');
const oid = createOrderId('xyz-456');
getUser(uid); // OK
getUser(oid); // Error: Argument of type 'OrderId' is not assignable to 'UserId'

The brand property (__brand) exists only at compile time. It is erased during JavaScript emission, so there is no runtime overhead.

\blacksquare

Example 2: Recursive Conditional Type for Deep Readonly

Section titled “Example 2: Recursive Conditional Type for Deep Readonly”

Problem. Implement DeepReadonly<T> that makes all nested properties readonly using recursive conditional types.

Solution.

type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
host: string;
ports: { http: number; https: number };
features: string[];
}
type FrozenConfig = DeepReadonly<Config>;
// Equivalent to:
// {
// readonly host: string;
// readonly ports: { readonly http: number; readonly https: number };
// readonly features: readonly string[];
// }

The conditional type recurses into objects while leaving primitives and functions untouched. Arrays become readonly string[] because string[] extends object.

\blacksquare

  • Conditional types: T extends U ? X : Y enables type-level branching; distribution over unions is automatic for naked type parameters.
  • Mapped types: { [K in keyof T]: F<T[K]> } transform all properties; as clauses filter and remap keys.
  • Template literal types: `${A}-${B}` create string types from unions, enabling type-safe string manipulation.
  • Branded types encode semantic distinctions (e.g., UserId vs OrderId) without runtime cost.
  • Recursive type aliases (TypeScript 4.1+) enable deep utility types like DeepReadonly, DeepPartial.

Advanced patterns combine TypeScript’s type-level features to create sophisticated abstractions. Discriminated unions with exhaustive checking ensure every variant is handled, using never to prove completeness. Branded types add semantic meaning to primitive types without runtime cost. Builder patterns use method chaining with return types to enforce required steps. Recursive types like DeepPartial and DeepReadonly traverse nested structures, while variadic tuples enable type-safe function composition.

  • [[typescript/generics]] - Conditional and mapped type foundations
  • [[typescript/advanced-types]] - Type-level arithmetic and template literals
  • [[typescript/classes]] - Structural versus nominal typing trade-offs
  • [[typescript/error-handling]] - Result patterns and exhaustive switch statements