TypeScript Advanced Types
TypeScript has an advanced type system. It lets you describe complex relationships between types with precision.
These features help you build applications that are robust, easy to maintain, and safe from many type errors.
Advanced TypeScript Types
This page gives an overview of the main advanced type features:
- Mapped Types: transform the properties of an existing type
- Conditional Types: pick a type based on a condition
- Template Literal Types: build string types with template syntax
- Utility Types: built-in helpers for common transformations
- Recursive Types: types that refer to themselves, for tree-like data
- Type Guards and Type Predicates: checks that narrow a type at runtime
- Type Inference: pattern matching inside types with
infer
Mapped Types
A mapped type creates a new type by walking over the properties of an existing type and transforming each one.
Basic Mapped Type
One template changes every property of an object type into a new type. Here every property of User becomes a boolean.
boolean.
Declare a generic Flags<T> mapped over keyof T, a User interface with id: number, name: string and email: string, and a UserFlags = Flags<User> alias; add a comment showing the equivalent object type. The file only declares types and prints nothing.
Output: TypeScript file and captured output
// Convert all properties to boolean
type Flags<T> = {
[K in keyof T]: boolean;
};
interface User {
id: number;
name: string;
email: string;
}
type UserFlags = Flags<User>;
// Equivalent to:
// {
// id: boolean;
// name: boolean;
// email: boolean;
// }
Mapped Type Modifiers
While mapping, you can add or remove the readonly and ? (optional) modifiers on every key. A + adds a modifier and a - removes it.
readonly and ? modifiers.
Declare a Todo interface (title: string, description: string, completed: boolean), an OptionalTodo mapped type that makes every property optional, a generic Concrete<T> that strips both readonly and ? with -readonly and -?, and a ReadonlyRequired<T> that adds +readonly and removes ?. Only type declarations, no output.
Output: TypeScript file and captured output
// Make all properties optional
interface Todo {
title: string;
description: string;
completed: boolean;
}
type OptionalTodo = {
[K in keyof Todo]?: Todo[K];
};
// Remove 'readonly' and '?' modifiers
type Concrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
// Add 'readonly' and 'required' to all properties
type ReadonlyRequired<T> = {
+readonly [K in keyof T]-?: T[K];
};
Key Remapping
With the as clause you can rename keys while mapping, or filter keys out by mapping them to never. String helpers like Capitalize and conditional checks make this possible.
as clause.
Declare a User interface (id: number, name: string, email: string), a generic Getters<T> that renames each key to get plus the capitalized key name (using Capitalize<string & K>) with a () => T[K] value, a UserGetters = Getters<User> alias with a comment showing the resulting shape, and a MethodsOnly<T> type that keeps only keys whose values extend Function by mapping the others to never. Types only, prints nothing.
Output: TypeScript file and captured output
interface User {
id: number;
name: string;
email: string;
}
// Add prefix to all property names
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
// Filter out properties
type MethodsOnly<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
Conditional Types
A conditional type chooses between two types depending on a condition. It works like an if statement, but at the type level.
Basic Conditional Types
The form T extends U ? X : Y picks X when T is assignable to U, and Y otherwise. The check happens entirely at the type level.
infer.
Declare IsString<T> as T extends string ? true : false, then aliases A, B, C, D for string, number, the literal 'hello' and string | number, with comments noting the results true, false, true and boolean. Also declare ArrayElement<T> that infers the element type U of an array type and a Numbers = ArrayElement<number[]> alias. Types only, no output.
Output: TypeScript file and captured output
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true
type D = IsString<string | number>; // boolean
// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = ArrayElement<number[]>; // number
Infer Keyword
Inside the condition of a conditional type, infer introduces a new type variable and captures part of the type being checked. This is how the built-in ReturnType and Parameters helpers work.
infer keyword captures parts of a type inside a conditional type by re-creating four built-in helpers.
Declare ReturnType<T> (infers the return type R of a function type, else any), Parameters<T> (infers the parameter tuple P, else never), ConstructorParameters<T> constrained to constructor types and inferring the constructor parameter tuple, and InstanceType<T> inferring the instance type of a constructor. Types only, nothing is printed; make the file a module so the names do not clash with the built-in globals.
Output: TypeScript file and captured output
// Get return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Get parameter types as a tuple
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Get constructor parameter types
type ConstructorParameters<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never;
// Get instance type from a constructor
type InstanceType<T extends new (...args: any) => any> =
T extends new (...args: any) => infer R ? R : any;
// Make this file a module so its type names do not clash with the built-in global ones
export {};
Distributed Conditional Types
When the checked type is a bare type parameter, a conditional type is applied to each member of a union separately. This is called distribution. Wrapping the type in a tuple, like [T] extends [any], stops the distribution so the union is handled as one type.
ToArrayNonDist<T> as T extends any ? T[] : never and StrOrNumArr = ToArrayNonDist<string | number>, then ToArray<T> written with the tuple wrapper [T] extends [any] ? T[] : never and StrOrNumArr2 = ToArray<string | number>, each with a comment showing the resulting type. Finally declare FilterStrings<T> that keeps only string members and a Letters alias applied to 'a' | 'b' | 1 | 2 | 'c'. Types only, no output.
Output: TypeScript file and captured output
// Without distribution
type ToArrayNonDist<T> = T extends any ? T[] : never;
type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[]
// With distribution
type ToArray<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr2 = ToArray<string | number>; // string[] | number[]
// Filter out non-string types
type FilterStrings<T> = T extends string ? T : never;
type Letters = FilterStrings<'a' | 'b' | 1 | 2 | 'c'>; // 'a' | 'b' | 'c'
Template Literal Types
Template literal types build new string types using the same backtick syntax as JavaScript template strings.
Basic Template Literal Types
A template literal type limits a string to a pattern. Combined with unions, it produces every possible combination of the parts. Assigning a string that does not match the pattern is a compile error.
Greeting type equal to the template `Hello, ${string}`, assign the valid value 'Hello, World!' to validGreeting, and assign 'Hi there!' to invalidGreeting marked // Error so the file deliberately fails type-checking. Then declare Color ('red' | 'green' | 'blue') and Size ('small' | 'medium' | 'large') unions and a Style template type `${Color}-${Size}` with a comment listing the nine resulting members. Expected result: a compile error for the invalid greeting.
Output: TypeScript file and captured output
type Greeting = `Hello, ${string}`;
const validGreeting: Greeting = 'Hello, World!';
const invalidGreeting: Greeting = 'Hi there!'; // Error
// With unions
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';
type Style = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | 'red-large' |
// 'green-small' | 'green-medium' | 'green-large' |
// 'blue-small' | 'blue-medium' | 'blue-large'
String Manipulation Types
TypeScript ships four helpers that change the case of string literal types: Uppercase, Lowercase, Capitalize and Uncapitalize. They are handy inside template literal types.
T1 to T4 using Uppercase<'hello'>, Lowercase<'WORLD'>, Capitalize<'typescript'> and Uncapitalize<'TypeScript'> with comments showing the results, then an EventType union ('click' | 'change' | 'keydown') and an EventHandler template type that prefixes on to the capitalized event names. Types only, no output.
Output: TypeScript file and captured output
// Built-in string manipulation types
type T1 = Uppercase<'hello'>; // 'HELLO'
type T2 = Lowercase<'WORLD'>; // 'world'
type T3 = Capitalize<'typescript'>; // 'Typescript'
type T4 = Uncapitalize<'TypeScript'>; // 'typeScript'
// Create an event handler type
type EventType = 'click' | 'change' | 'keydown';
type EventHandler = `on${Capitalize<EventType>}`;
// 'onClick' | 'onChange' | 'onKeydown'
Advanced Patterns
Template literal types can be combined with infer and key remapping. That lets you pull information out of a string type, such as route parameters, or generate a whole API of handler names from an event map.
infer and key remapping.
Declare a recursive ExtractRouteParams<T> that matches `${string}:${infer Param}/${infer Rest}` (and the single-parameter form) to build an object type with one string property per route parameter, plus a Params alias for '/users/:userId/posts/:postId'. Then declare an EventMap type (click with x/y numbers, change as string, keydown with key: string and code: number) and an EventHandlers mapped type that renames each key to on plus the capitalized name and types it as a handler receiving EventMap[K]. Types only, no output.
Output: TypeScript file and captured output
// Extract route parameters
type ExtractRouteParams<T> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<`${Rest}`>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string; }
// Create a type-safe event emitter
type EventMap = {
click: { x: number; y: number };
change: string;
keydown: { key: string; code: number };
};
type EventHandlers = {
[K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void;
};
Utility Types
TypeScript comes with many built-in utility types. They cover the most common type transformations so you do not have to write them yourself.
Common Utility Types
Partial, Required, Readonly, Pick and Omit are the ones you will use most. You can also read a single property type with User['id'] and all key names with keyof User.
User interface with id: number, name: string, email: string and createdAt: Date, then aliases PartialUser = Partial<User>, RequiredUser = Required<PartialUser>, ReadonlyUser = Readonly<User>, UserPreview = Pick<User, 'id' | 'name'> and UserWithoutEmail = Omit<User, 'email'>, each with a short comment. Finish with UserId = User['id'] and UserKeys = keyof User and comments showing the results. Types only, no output.
Output: TypeScript file and captured output
// Basic types
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
// Make all properties optional
type PartialUser = Partial<User>;
// make all properties required
type RequiredUser = Required<PartialUser>;
// make all properties read-only
type ReadonlyUser = Readonly<User>;
// pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// omit specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// extract property types
type UserId = User['id']; // number
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'createdAt'
Advanced Utility Types
Exclude removes members from a union and Extract keeps only the matching ones. You can also write your own helpers, like a Mutable type that strips readonly from every property.
NonNullable<T> conditional type that maps null | undefined to never, a Numbers union 1 | 2 | 3 | 'a' | 'b', JustNumbers = Exclude<Numbers, string> and JustStrings = Extract<Numbers, string> with result comments, then object types A (a: string; b: number; c: boolean) and B (a: string; b: number) and C = Omit<A, keyof B>, and finally a Mutable<T> mapped type using -readonly. Types only, no output; make the file a module so NonNullable does not clash with the global one.
Output: TypeScript file and captured output
// Create a type that excludes null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// Exclude types from a union
type Numbers = 1 | 2 | 3 | 'a' | 'b';
type JustNumbers = Exclude<Numbers, string>; // 1 | 2 | 3
// Extract types from a union
type JustStrings = Extract<Numbers, string>; // 'a' | 'b'
// Get the type that is not in the second type
type A = { a: string; b: number; c: boolean };
type B = { a: string; b: number };
type C = Omit<A, keyof B>; // { c: boolean }
// Create a type with all properties as mutable
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Make this file a module so its type names do not clash with the built-in global ones
export {};
Recursive Types
A recursive type refers to itself in its own definition. This is the natural way to describe tree-like data, where a node can contain more nodes of the same shape.
Basic Recursive Type
A binary tree node holds optional left and right nodes of the same type. A JSON value can contain arrays and objects of more JSON values. A comment holds an array of replies that are also comments.
BinaryTree<T> with a value and optional left/right subtrees of the same type, a JSONValue union of string, number, boolean, null, JSONValue[] and an object with JSONValue values, and a Comment type with id, content, a replies: Comment[] array and createdAt: Date. Types only, no output; make the file a module so Comment does not clash with the DOM global.
Output: TypeScript file and captured output
// Simple binary tree
type BinaryTree<T> = {
value: T;
left?: BinaryTree<T>;
right?: BinaryTree<T>;
};
// JSON-like data structure
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// Nested comments
type Comment = {
id: number;
content: string;
replies: Comment[];
createdAt: Date;
};
// Make this file a module so its type names do not clash with the built-in global ones
export {};
Advanced Recursive Types
The same idea describes a linked list, a directory tree whose children are files or more directories, a state machine whose transitions lead to other states, and even a function type that accepts itself.
LinkedList<T> with value and next: LinkedList<T> | null, a File type (type: 'file', name, size) and a Directory type (type: 'directory', name, children: (File | Directory)[]), a State type whose transitions map event names to other States, and a RecursiveFunction<T> function type that accepts T | RecursiveFunction<T>. Types only, no output; make the file a module so File does not clash with the DOM global.
Output: TypeScript file and captured output
// Type for a linked list
type LinkedList<T> = {
value: T;
next: LinkedList<T> | null;
};
// Type for a directory structure
type File = {
type: 'file';
name: string;
size: number;
};
type Directory = {
type: 'directory';
name: string;
children: (File | Directory)[];
};
// Type for a state machine
type State = {
value: string;
transitions: {
[event: string]: State;
};
};
// Type for a recursive function
type RecursiveFunction<T> = (x: T | RecursiveFunction<T>) => void;
// Make this file a module so its type names do not clash with the built-in global ones
export {};
Best Practices
When to use advanced types:
- Use mapped types when you need to change many properties of an object type at once
- Use conditional types when one type depends on another type
- Use template literal types for string patterns and string matching
- Use utility types for common transformations — prefer the built-in ones when they fit
- Use recursive types for tree-like or nested data
Performance considerations:
- Deeply nested recursive types can slow down the TypeScript compiler
- Very large union types (100+ members) can cause performance problems
- Break complex types into smaller type aliases
Common Pitfalls
Type inference issues:
- Conditional types distribute over union types, which can be surprising
inferbehaves differently depending on where it is used- Some utility types do not work well with
anyorunknown
Maintainability:
- Overusing complex types makes code hard to read
- Explain complex type transformations with comments
- For very complex cases, a type assertion or a helper function can be simpler