What are advanced TypeScript types?
Advanced types are the tools that let you transform, compose and manipulate types at the type level. Instead of manually defining parallel types for every variation, you derive them automatically from existing ones.
If basics give you types and generics let you write reusable code, advanced types let you build a type system that describes your data precisely — catching errors that simpler approaches miss.
The three pillars are utility types (built-in transforms), conditional types (type-level logic) and mapped types (property transformation). Together they form a small language for working with types.
Utility types
TypeScript ships with built-in types that modify other types:
Partial and Required
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; }
// All properties required
type StrictUser = Required<Partial<User>>;
// { id: number; name: string; email: string; }
Pick and Omit
// Extract specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Remove specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }
Record
// Create object type with specific keys and values
type Scores = Record<string, number>;
// { [key: string]: number }
const scores: Scores = { math: 95, science: 87 };
// With union keys
type StatusMap = Record<"idle" | "loading" | "done", string>;
Extract and Exclude
type Status = "idle" | "loading" | "success" | "error";
// Keep only matching members
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"
// Remove matching members
type InactiveStatus = Exclude<Status, "loading" | "success">;
// "idle" | "error"
ReturnType and Parameters
function createUser(name: string, age: number) {
return { name, age, id: Date.now() };
}
type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }
type Args = Parameters<typeof createUser>;
// [string, number]
NonNullable
type Maybe = string | null | undefined;
type Definite = NonNullable<Maybe>;
// string
Mapped types
Mapped types iterate over the keys of an existing type and transform each property:
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
type MutableUser = ReadOnly<User>;
// All properties are readonly
Adding modifiers
// Add optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Remove optional
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
The - and + operators remove or add modifiers during mapping.
Key remapping with as
TypeScript 4.1+ lets you remap keys during mapping:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }
Conditional types
Conditional types express logic at the type level:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
The infer keyword
infer extracts a type from within another type:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
Practical examples
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number
// Unwrap promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Deep = Awaited<Promise<Promise<string>>>; // string
// Extract object property types
type ValuesOf<T> = T[keyof T];
type UserValues = ValuesOf<User>; // number | string
Distributive conditional types
When a conditional type distributes over a union, each member is checked separately:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] — not (string | number)[]
This is useful for types like Extract and Exclude that filter union members.
Template literal types
Template literal types manipulate strings at the type level:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
With mapped types
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: () => void;
};
type MouseEvents = EventHandlers<"click" | "hover" | "focus">;
// { onClick: () => void; onHover: () => void; onFocus: () => void; }
String pattern matching
type CSSProperty = `${string}-${string}`;
type ValidCSS = CSSProperty; // any string with a hyphen
Discriminated unions
Discriminated unions are the most important advanced pattern for modeling state:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function handleState(state: RequestState<User>) {
switch (state.status) {
case "idle":
return "Ready";
case "loading":
return "Loading...";
case "success":
return state.data.name; // TypeScript knows data exists
case "error":
return state.error; // TypeScript knows error exists
}
}
The status property is the discriminant. TypeScript narrows the type automatically in each branch.
Branded types
Branded types prevent mixing types that have the same structure:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function branded<T extends string>(value: T): T & { readonly __brand: unique symbol } {
return value as any;
}
const userId = branded<UserId>("user-123");
const orderId = branded<OrderId>("order-456");
function getUser(id: UserId) { /* ... */ }
getUser(userId); // OK
getUser(orderId); // Error: OrderId is not assignable to UserId
Best practices
- Use utility types instead of manually duplicating type definitions.
- Prefer discriminated unions over type assertions for state modeling.
- Build types from small, composable pieces instead of large monolithic definitions.
- Use conditional types sparingly — if a type is hard to read, simplify the API.
- Use
inferto extract types from libraries instead of manually typing return values. - Keep the source of truth in one place and derive everything else.
- Use branded types when you need nominal typing for IDs or similar values.
- Treat complex types as a last resort — simple types are easier to maintain.
Common mistakes
- Over-engineering types when simpler ones would work.
- Creating deeply nested conditional types that are impossible to debug.
- Using
asassertions instead of proper type narrowing. - Not using utility types, leading to duplicated type definitions.
- Making types too complex for the benefit they provide.
- Forgetting that types are erased at runtime — no runtime type checking.
- Not documenting complex types with comments explaining the intent.
What to learn next
You now understand advanced types: utility types, conditional types, mapped types and template literals. From here the natural next step is TypeScript for the fundamentals — annotations, inference, interfaces and generics. Pick a project, refactor some types and let the practice compound.