Advanced types
TypeScript’s advanced types admit substantial type-level computation: mapped types (transforming property types across a key set), conditional types (T extends X ? Y : Z), template literal types (string-typed pattern construction), the infer keyword (extracting types from positions), and the utility types (Partial, Required, Pick, Omit, Record, ReturnType, Awaited, etc.). The combination admits expressing substantial type-level logic — the type system is itself a small functional programming language operating on types. The conventional discipline is to lean on the standard utility types for routine transformations and to reserve elaborate conditional/mapped types for genuinely generic library code.
Mapped types
A mapped type admits transforming the keys and values of an existing type:
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface Person {
name: string;
age: number;
email: string;
}
type OptionalPerson = Optional<Person>;
// {
// name?: string | undefined;
// age?: number | undefined;
// email?: string | undefined;
// }
The form [K in keyof T]: ... iterates over the keys of T, producing a property for each key. The mechanism admits substantial structural transformations.
Mapping modifiers
The ? and readonly modifiers admit adding or removing optionality and immutability:
// Add optional and readonly:
type ReadonlyOptional<T> = {
readonly [K in keyof T]?: T[K];
};
// Remove optional (`-?`) and readonly (`-readonly`):
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
The -? and -readonly admit “remove this modifier”; the bare ? and readonly admit “add this modifier”.
Key remapping with as
Since TS 4.1, mapped types admit key remapping via as:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }
The mechanism admits substantial key transformation; conventional for builder-pattern types and DSLs.
For filtering keys, as with a conditional admits “include/exclude”:
type FilterKeys<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface User {
name: string;
age: number;
email: string;
active: boolean;
}
type StringKeys = FilterKeys<User, string>;
// { name: string; email: string }
The K in keyof T as ... ? K : never admits “keep K if condition; drop otherwise” — a key with type never is excluded from the resulting type.
Conditional types
The form T extends X ? Y : Z admits type-level branching:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Distributive conditional types
Conditional types are distributive over unions when the checked type is a “naked” type parameter:
type ExtractString<T> = T extends string ? T : never;
type A = ExtractString<string | number | boolean>; // string
// Distributes: (string extends string ? string : never)
// | (number extends string ? number : never)
// | (boolean extends string ? boolean : never)
// = string | never | never
// = string
To prevent distribution, wrap the parameter in a tuple:
type IsUnion<T> = [T] extends [string] ? true : false;
type A = IsUnion<string>; // true
type B = IsUnion<string | number>; // false (the tuple form does not distribute)
The mechanism is conventional for substantial type-level logic.
Extract and Exclude
The standard library’s distributive utilities:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type A = Extract<"a" | "b" | "c", "a" | "c">; // "a" | "c"
type B = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type C = Extract<string | number, string>; // string
type D = Exclude<string | number, string>; // number
The infer keyword
infer T introduces a type variable in a conditional’s extends clause:
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type A = ReturnType<() => string>; // string
type B = ReturnType<(x: number) => boolean>; // boolean
type Parameters<F> = F extends (...args: infer P) => any ? P : never;
type C = Parameters<(x: number, y: string) => void>; // [number, string]
type ArrayElement<A> = A extends (infer T)[] ? T : never;
type D = ArrayElement<string[]>; // string
type E = ArrayElement<number[][]>; // number[]
The infer admits extracting a type from a positional template. The mechanism is one of TypeScript’s most powerful type-level operators.
Awaited
The standard Awaited<T> recursively unwraps promises:
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<string>; // string
Template literal types
Since TS 4.1, template literal types admit pattern construction at the type level:
type Greeting = `Hello, ${string}!`;
type Specific = "Hello, world!";
const a: Greeting = "Hello, world!"; // OK
const b: Greeting = "Hi, there!"; // ERROR
type Direction = `${"north" | "south"}-${"east" | "west"}`;
// "north-east" | "north-west" | "south-east" | "south-west"
type CSSValue = `${number}px` | `${number}em` | `${number}%`;
const v: CSSValue = "100px"; // OK
const w: CSSValue = "100"; // ERROR
The mechanism admits substantial precision for string-formatted types — CSS values, route parameters, event names, etc.
Template literal type extraction
type ExtractParam<T> = T extends `${string}{${infer P}}${string}` ? P : never;
type A = ExtractParam<"GET /users/{id}">; // "id"
type B = ExtractParam<"POST /posts/{postId}/comments/{commentId}">; // "commentId"
// (only the last; needs recursion for all)
// Recursive extraction:
type ExtractAllParams<T> =
T extends `${string}{${infer P}}${infer Rest}`
? P | ExtractAllParams<Rest>
: never;
type C = ExtractAllParams<"/users/{id}/posts/{postId}">;
// "id" | "postId"
The pattern is conventional in route-typing and template engines.
Built-in template-string utilities
type A = Uppercase<"hello">; // "HELLO"
type B = Lowercase<"HELLO">; // "hello"
type C = Capitalize<"hello">; // "Hello"
type D = Uncapitalize<"Hello">; // "hello"
The standard utility types
TypeScript provides substantial utility types for common transformations:
Partial<T> — make all properties optional
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>;
// { name?: string; age?: number }
function update(user: User, changes: Partial<User>): User {
return { ...user, ...changes };
}
Required<T> — make all properties required
interface Config {
host?: string;
port?: number;
}
type StrictConfig = Required<Config>;
// { host: string; port: number }
Readonly<T> — make all properties readonly
interface User {
name: string;
age: number;
}
type ImmutableUser = Readonly<User>;
// { readonly name: string; readonly age: number }
Pick<T, K> — select keys
interface User {
name: string;
age: number;
email: string;
password: string;
}
type PublicUser = Pick<User, "name" | "email">;
// { name: string; email: string }
Omit<T, K> — exclude keys
type SafeUser = Omit<User, "password">;
// { name: string; age: number; email: string }
Record<K, V> — typed record
type Permissions = Record<string, boolean>;
type Counts = Record<"active" | "inactive", number>;
const counts: Counts = {
active: 10,
inactive: 5,
};
Exclude<T, U> and Extract<T, U>
type A = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type B = Extract<string | number, string>; // string
NonNullable<T> — exclude null and undefined
type A = NonNullable<string | null | undefined>; // string
ReturnType<F> and Parameters<F>
function fn(x: number, y: string): boolean { /* ... */ }
type R = ReturnType<typeof fn>; // boolean
type P = Parameters<typeof fn>; // [number, string]
Awaited<T> — unwrap promises
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
ConstructorParameters<C> and InstanceType<C>
class User {
constructor(public name: string, public age: number) {}
}
type Args = ConstructorParameters<typeof User>; // [name: string, age: number]
type Instance = InstanceType<typeof User>; // User
Common patterns
Deep partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
server: {
host: string;
port: number;
ssl: { cert: string; key: string };
};
}
type ConfigPatch = DeepPartial<Config>;
// All properties at all depths are optional.
Deep readonly
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
Function type extraction
type FnReturn<F> = F extends (...args: any[]) => infer R ? R : never;
interface Service {
getUser: (id: string) => Promise<User>;
createPost: (data: PostData) => Promise<Post>;
}
type GetUserResult = FnReturn<Service["getUser"]>; // Promise<User>
Discriminated union to map
type ActionMap<A extends { type: string }> = {
[K in A["type"]]: Extract<A, { type: K }>;
};
type Action =
| { type: "INCREMENT"; by: number }
| { type: "DECREMENT"; by: number }
| { type: "RESET" };
type ByType = ActionMap<Action>;
// {
// INCREMENT: { type: "INCREMENT"; by: number };
// DECREMENT: { type: "DECREMENT"; by: number };
// RESET: { type: "RESET" };
// }
Type-safe event emitter
type EventMap = {
click: { x: number; y: number };
keypress: { key: string };
scroll: { offset: number };
};
class TypedEmitter<E extends Record<string, any>> {
on<K extends keyof E>(event: K, handler: (data: E[K]) => void): void { /* ... */ }
emit<K extends keyof E>(event: K, data: E[K]): void { /* ... */ }
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("click", e => console.log(e.x, e.y)); // OK
emitter.emit("click", { x: 1, y: 2 }); // OK
// emitter.emit("click", { x: 1 }); // ERROR: missing y
Object key transformation
type CamelToSnake<S extends string> =
S extends `${infer Head}${infer Tail}`
? Head extends Lowercase<Head>
? `${Head}${CamelToSnake<Tail>}`
: `_${Lowercase<Head>}${CamelToSnake<Tail>}`
: S;
type SnakeKeys<T> = {
[K in keyof T as K extends string ? CamelToSnake<K> : K]: T[K];
};
interface User {
firstName: string;
lastName: string;
emailAddress: string;
}
type SnakeUser = SnakeKeys<User>;
// {
// first_name: string;
// last_name: string;
// email_address: string;
// }
Pick by value type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
a: string;
b: number;
c: string;
d: boolean;
}
type Strings = PickByValue<Mixed, string>;
// { a: string; c: string }
Merge two types
type Merge<A, B> = Omit<A, keyof B> & B;
interface Defaults {
timeout: number;
retries: number;
verbose: boolean;
}
interface Overrides {
timeout: number;
apiKey: string;
}
type Config = Merge<Defaults, Overrides>;
// { retries: number; verbose: boolean; timeout: number; apiKey: string }
Tuple type manipulation
type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer R] ? R : [];
type A = Head<[1, 2, 3]>; // 1
type B = Tail<[1, 2, 3]>; // [2, 3]
Conditional return type
function find<T, K extends keyof T>(
items: readonly T[],
key: K,
value: T[K] | null,
): T | undefined {
return items.find(item => item[key] === value);
}
Recursive type for JSON
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];
Brand types
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type GroupId = Brand<number, "GroupId">;
function makeUserId(n: number): UserId { return n as UserId; }
function makeGroupId(n: number): GroupId { return n as GroupId; }
const u = makeUserId(42);
const g = makeGroupId(99);
function lookup(id: UserId) { /* ... */ }
lookup(u); // OK
// lookup(g); // ERROR
// lookup(42); // ERROR
A note on the conventional discipline
The contemporary TypeScript advanced types advice:
- Use the standard utility types — they cover most routine cases.
- Use mapped types for structural transformations.
- Use conditional types sparingly — for genuinely generic library code.
- Use
inferin conditional types for type extraction. - Use template literal types for substantial string-pattern types.
- Use brand types for nominal type safety.
- Avoid over-engineering — explicit types are conventionally clearer than substantial type-level computation.
- Trust the inference — explicit type arguments are usually unnecessary.
The combination — mapped types, conditional types, the infer keyword, template literal types, and the standard utility types — is the substance of TypeScript’s advanced type surface. The discipline produces substantial expressiveness; the trade-off is the type-level code’s relative opacity. The conventional advice is to write plain types where possible and reach for the advanced surface only when type-level computation is genuinely needed.