Advanced Types
With basic types, interfaces, generics, and utility types under your belt, you are ready for the parts of TypeScript's
type system that feel almost like a programming language unto themselves. Conditional types, mapped types, template
literal types, and the infer keyword let you write type-level logic that would be impossible to express with simpler
tools.
Conditional types
Conditional types choose between two types based on a condition, using the syntax T extends U ? X : Y:
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true (string literals extend string)
The power of conditional types comes from combining them with generics:
// Flatten arrays -- if T is an array, return the element type; otherwise return T
type Flatten<T> = T extends (infer U)[] ? U : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
type Obj = Flatten<{ a: 1 }[]>; // { a: 1 }
// Unwrap a Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Result = Awaited<Promise<Promise<string>>>; // string
Distributive conditional types
When a conditional type is applied to a union, it distributes over each member:
type NonArray<T> = T extends unknown[] ? never : T;
type Filtered = NonArray<string | number[] | boolean | string[]>;
// Distributed as:
// NonArray<string> = string (not an array)
// NonArray<number[]> = never (is an array)
// NonArray<boolean> = boolean (not an array)
// NonArray<string[]> = never (is an array)
// Result: string | boolean
// This is how Exclude is implemented:
type MyExclude<T, U> = T extends U ? never : T;
type Result = MyExclude<"a" | "b" | "c", "b">; // "a" | "c"
Practical conditional types
// Get the type of a promise's resolved value, or the type itself if not a Promise
type Resolved<T> = T extends PromiseLike<infer U> ? U : T;
// Extract function arguments based on position
type FirstArg<T extends (...args: never[]) => unknown> =
T extends (first: infer F, ...rest: never[]) => unknown ? F : never;
function greet(name: string, age: number): string {
return `Hello ${name}`;
}
type NameType = FirstArg<typeof greet>; // string
// Make methods optional in an interface
type Implementable<T> = {
[K in keyof T]: T[K] extends (...args: never[]) => unknown
? T[K] | undefined
: T[K];
};
The infer keyword
infer declares a type variable inside a conditional type. It lets you extract parts of a type:
// Extract the return type of a function (how ReturnType<T> works internally)
type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
// Extract element type from an array
type ElementType<T> = T extends (infer E)[] ? E : never;
// Extract the value type from a Promise
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
// Extract the last element of a tuple
type LastElement<T extends unknown[]> =
T extends [...infer _, infer Last] ? Last : never;
type L = LastElement<[string, number, boolean]>; // boolean
// Extract the first element of a tuple
type Head<T extends unknown[]> =
T extends [infer H, ...infer _] ? H : never;
type H = Head<[string, number, boolean]>; // string
// Tail of a tuple
type Tail<T extends unknown[]> =
T extends [infer _, ...infer Rest] ? Rest : never;
type Rest = Tail<[string, number, boolean]>; // [number, boolean]
Inferring parameter types from a callback
// Given an event emitter, extract the event callback's argument types
type EventArgs<T extends (arg: never) => void> =
T extends (arg: infer A) => void ? A : never;
type ClickArgs = EventArgs<(event: MouseEvent) => void>; // MouseEvent
// Infer the type from nested generics
type UnwrapPromiseArray<T> =
T extends Array<Promise<infer U>> ? U[] : never;
type StringArray = UnwrapPromiseArray<Promise<string>[]>; // string[]
Mapped types
Mapped types iterate over the keys of a type and transform each property:
// The syntax: { [K in keyof T]: NewType }
// Make all properties optional (how Partial<T> works)
type MyPartial<T> = { [K in keyof T]?: T[K] };
// Make all properties required (how Required<T> works)
type MyRequired<T> = { [K in keyof T]-?: T[K] };
// Make all properties readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Remove readonly from all properties
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
Remapping with as
TypeScript 4.1 added key remapping in mapped types using as:
// Prefix all keys with "get"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
email: string;
age: number;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getEmail: () => string;
// getAge: () => number;
// }
// Filter properties by type using remapping
type PickByType<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
score: number;
}
type StringProps = PickByType<Mixed, string>; // { name: string }
type NumberProps = PickByType<Mixed, number>; // { id: number; score: number }
Mapped types with conditional types
Combining mapped types and conditional types unlocks deep transformations:
// Wrap all properties in a Promise (for async versions)
type Async<T> = {
[K in keyof T]: Promise<T[K]>;
};
// Make nullable properties optional
type NullableToOptional<T> = {
[K in keyof T as null extends T[K] ? never : K]: T[K];
} & {
[K in keyof T as null extends T[K] ? K : never]?: Exclude<T[K], null>;
};
// Deep partial (recursively make all properties optional)
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
server: {
host: string;
port: number;
tls: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
pool: number;
};
}
type PartialConfig = DeepPartial<Config>;
// Every nested property is optional
const override: PartialConfig = {
server: { port: 8080 },
// database is omitted, tls is omitted, etc.
};
Template literal types
Template literal types (TypeScript 4.1+) apply string template syntax at the type level:
type EventName = "click" | "focus" | "blur" | "change" | "submit";
// Generate "onClick", "onFocus", etc.
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange" | "onSubmit"
// CSS property pattern
type CssProp = "margin" | "padding" | "border";
type CssDirection = "top" | "right" | "bottom" | "left";
type CssValue = `${CssProp}-${CssDirection}`;
// "margin-top" | "margin-right" | ... | "border-left" (12 combinations)
// Event listener types
type DomEventMap = {
[K in `on${Capitalize<string & keyof GlobalEventHandlersEventMap>}`]:
K extends `on${infer E}`
? Lowercase<E> extends keyof GlobalEventHandlersEventMap
? GlobalEventHandlersEventMap[Lowercase<E>]
: Event
: never;
};
Template literals for route paths
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type ApiRoute = `/api/${ApiVersion}/${Resource}`;
// "/api/v1/users" | "/api/v1/posts" | ... | "/api/v2/comments"
function fetchResource(route: ApiRoute): Promise<unknown> {
return fetch(route).then(r => r.json());
}
fetchResource("/api/v1/users"); // OK
// fetchResource("/api/v3/users"); // Error: not a valid ApiRoute
// Extract path parameters
type ExtractParams<Path extends string> =
Path extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
Recursive types
TypeScript supports types that reference themselves, which is essential for tree-like or nested structures:
// JSON type
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
const data: Json = {
name: "Alice",
age: 30,
hobbies: ["reading", "coding"],
address: {
city: "Berlin",
coordinates: [52.52, 13.405],
},
};
// Nested comment structure
interface Comment {
id: number;
author: string;
content: string;
createdAt: Date;
replies: Comment[];
}
// File system tree
type FileSystemNode =
| { type: "file"; name: string; size: number }
| { type: "directory"; name: string; children: FileSystemNode[] };
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends (...args: never[]) => unknown
? T[K] // functions stay as-is
: DeepReadonly<T[K]> // recurse into objects
: T[K]; // primitives stay as-is
};
Recursive conditional types (TypeScript 4.1+)
// Flatten deeply nested arrays
type DeepFlatten<T> = T extends (infer U)[]
? DeepFlatten<U>
: T;
type Result = DeepFlatten<number[][][]>; // number
// Paths to all leaf properties of an object
type Paths<T, D extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? Paths<T[K], `${D}${K}.`>
: `${D}${K}`;
}[keyof T & string];
interface Config {
server: { host: string; port: number };
database: { url: string };
}
type ConfigPaths = Paths<Config>;
// "server.host" | "server.port" | "database.url"
Real-world example: a typed event system
Putting conditional types, mapped types, and template literals together:
// Define events and their payload types
interface AppEventMap {
"user:login": { userId: number; timestamp: Date };
"user:logout": { userId: number };
"cart:add": { productId: number; quantity: number };
"cart:remove": { productId: number };
"order:placed": { orderId: number; total: number };
"error": { message: string; code?: string };
}
type EventName = keyof AppEventMap;
type EventPayload<E extends EventName> = AppEventMap[E];
type EventListener<E extends EventName> = (payload: EventPayload<E>) => void;
class TypedEventEmitter {
private listeners = new Map<string, EventListener<EventName>[]>();
on<E extends EventName>(event: E, listener: EventListener<E>): void {
const existing = (this.listeners.get(event) ?? []) as EventListener<E>[];
this.listeners.set(event, [...existing, listener] as EventListener<EventName>[]);
}
off<E extends EventName>(event: E, listener: EventListener<E>): void {
const existing = this.listeners.get(event) ?? [];
this.listeners.set(
event,
existing.filter(l => l !== listener) as EventListener<EventName>[],
);
}
emit<E extends EventName>(event: E, payload: EventPayload<E>): void {
const handlers = this.listeners.get(event) ?? [];
handlers.forEach(handler => (handler as EventListener<E>)(payload));
}
once<E extends EventName>(event: E, listener: EventListener<E>): void {
const wrapper: EventListener<E> = (payload) => {
listener(payload);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
const emitter = new TypedEventEmitter();
emitter.on("user:login", ({ userId, timestamp }) => {
// userId: number, timestamp: Date -- fully typed
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.emit("user:login", { userId: 1, timestamp: new Date() }); // OK
// emitter.emit("user:login", { userId: "wrong" }); // Error: string not assignable to number
Summary
- Conditional types (
T extends U ? X : Y) choose between types based on a condition, and distribute over unions inferextracts type variables from within a conditional type -- used to buildReturnType,Parameters, etc.- Mapped types iterate over object keys and transform each property -- the basis of all utility types
- Key remapping (
as) in mapped types filters or renames keys -- enablesPickByTypeand similar helpers - Template literal types combine string unions with template syntax to generate new string union types
- Recursive types reference themselves to describe trees, nested objects, and deeply structured data
- Combining these features lets you write type-level logic that enforces complex invariants at compile time
Next up: Modules & Declaration Files -- ES modules in TypeScript,
import/export, .d.ts declaration files, DefinitelyTyped, and writing your own declarations.