In this third episode of our TotalTypeScript-inspired series, we master Advanced TypeScript Metaprogramming. We explore Conditional Types, Distributive Conditional Types, pattern matching with the infer keyword, Template Literal Types, Recursive Types (DeepReadonly, DeepPartial), and Type-Level State Machines.
1. Conditional Types (T extends U ? X : Y)#
Conditional Types select one of two possible types based on a type relationship test:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<"hello">; // true
type Test2 = IsString<42>; // false
Real-World Example: Dynamic Return Types#
interface User { id: string; name: string }
// If IdOrIds is string, return User. If string[], return User[]
type GetUserResult<T extends string | string[]> = T extends string ? User : User[];
function getUser<T extends string | string[]>(idOrIds: T): GetUserResult<T> {
if (typeof idOrIds === "string") {
return { id: idOrIds, name: "Alice" } as GetUserResult<T>;
}
return [{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }] as GetUserResult<T>;
}
const singleUser = getUser("usr_101"); // Inferred type: User
const userList = getUser(["usr_101", "usr_102"]); // Inferred type: User[]
2. Distributive Conditional Types#
When conditional types act on a generic type parameter <T> that is a naked union, they automatically distribute over every member of the union:
type ToArray<T> = T extends any ? T[] : never;
// Distributes over 'string | number': (string extends any ? string[] : never) | (number extends any ? number[] : never)
type Result = ToArray<string | number>; // string[] | number[]
Preventing Distribution#
To prevent conditional types from distributing over unions, wrap the generic parameter in square brackets [T]:
type NonDistributiveToArray<T> = [T] extends [any] ? T[] : never;
type NonDistResult = NonDistributiveToArray<string | number>; // (string | number)[]
3. Pattern Matching with the infer Keyword#
The infer keyword allows you to declare a type variable within the extends clause of a conditional type, extracting nested types via pattern matching.
// Extract Element Type from an Array
type ElementOf<T> = T extends (infer U)[] ? U : T;
type Str = ElementOf<string[]>; // string
type Num = ElementOf<number>; // number
Unwrapping Promises (Awaited<T> Implementation)#
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
type NestedPromise = Promise<Promise<string>>;
type Unwrapped = MyAwaited<NestedPromise>; // string
Extracting Function Return Types (ReturnType<T> Implementation)#
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function calculateScore() {
return { score: 98, rank: "A+" };
}
type ScoreResult = MyReturnType<typeof calculateScore>; // { score: number; rank: string }
4. Template Literal Types and String Manipulation#
Template Literal Types combine string literals with generic parameters to compute dynamic string types at compile time:
type EventType = "click" | "hover" | "focus";
type Target = "button" | "input";
// Computes union of all combinations: "click_button" | "click_input" | "hover_button" ...
type EventDescriptor = `${EventType}_${Target}`;Built-in String Manipulation Utilities#
TypeScript includes four built-in type utilities for string transformation:
type Route = "user" | "order" | "product";
type GetterName = `get${Capitalize<Route>}`; // "getUser" | "getOrder" | "getProduct"
type UpperRoute = Uppercase<Route>; // "USER" | "ORDER" | "PRODUCT"
Real-World Event Bus Typing#
type DynamicEvents = {
userCreated: { id: string; email: string };
orderPlaced: { orderId: string; total: number };
};
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (data: T[K]) => void;
};
const bus: EventHandlers<DynamicEvents> = {
onUserCreated: (data) => console.log(data.email),
onOrderPlaced: (data) => console.log(data.total),
};5. Recursive Types: Deep Readonly and Deep Partial#
Conditional types and mapped types can be recursive, applying transformations through nested object hierarchies:
type DeepReadonly<T> = T extends Function | boolean | number | string | null | undefined
? T
: T extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type NestedConfig = {
db: {
host: string;
ports: number[];
};
};
type ImmutableConfig = DeepReadonly<NestedConfig>;
// ImmutableConfig.db.ports.push(8080); // Compiler Error! ReadonlyArray!
6. Type-Level State Machine#
We can use template literal types and conditional types to construct type-safe state machines that validate state transitions at compile time:
type State = "idle" | "loading" | "success" | "error";
type ValidTransitions = {
idle: "loading";
loading: "success" | "error";
success: "idle";
error: "loading";
};
class StateMachine<TState extends State> {
constructor(private state: TState) {}
transition<TNext extends ValidTransitions[TState]>(nextState: TNext): StateMachine<TNext> {
return new StateMachine(nextState);
}
getState(): TState {
return this.state;
}
}
const machine = new StateMachine("idle");
const loadingMachine = machine.transition("loading"); // Valid!
const successMachine = loadingMachine.transition("success"); // Valid!
// Compiler Error! Cannot transition directly from 'idle' to 'success'!
// machine.transition("success");
Key Takeaways#
- Use Conditional Types for Computations: Use
T extends U ? X : Yto compute return types dynamically based on input parameter shapes. - Master
inferPattern Matching: Useinferinsideextendsclauses to extract promise payloads, return types, or tuple elements effortlessly. - Build String Types with Template Literals: Combine
${T}_${U}andCapitalize<T>to generate typed event handlers and routing maps without manual duplication. - Enforce State Safety: Build Type-Level State Machines to prevent invalid application state transitions at compile time.

