Skip to main content

Mapped Types and Conditional Types

In this chapter, you learn about TypeScript's advanced type features, Mapped Types and Conditional Types. Once you understand them, you can create your own utility types.

What you learn in this chapter

  • The basics and mechanics of Mapped Types
  • Adding and removing modifiers (readonly, ?)
  • The basic syntax of Conditional Types
  • Type inference with the infer keyword
  • Template Literal Types
  • Creating custom utility types
warning

Advanced material: The content of this chapter is an advanced topic. It is not always necessary for everyday TypeScript development, but it is useful for library development and defining complex types. Beginners may skip it once and come back after learning the other chapters.

info

Mapped Types and Conditional Types are the techniques used to implement TypeScript's built-in utility types (Partial, Required, ReturnType, etc.). Understanding them enables more advanced type programming.

What are Mapped Types

Mapped Types are a feature like a "type-level for loop" that generates a new type from an existing one. You can apply a transformation to each property of an object type.

Basic Mapped Types

// The original type
interface User {
id: number;
name: string;
email: string;
}

// Make all properties optional
// [K in keyof User] iterates over each property name of User
type PartialUser = {
[K in keyof User]?: User[K];
};
// Result:
// {
// id?: number;
// name?: string;
// email?: string;
// }

// Make all properties read-only
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// Result:
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }

Explanation of the syntax:

  • [K in keyof User]: take out User's property names in order (K = "id", "name", "email")
  • User[K]: get the type corresponding to property K (User["id"] = number, etc.)
  • ?: add the optional modifier
  • readonly: add the read-only modifier

Generic Mapped Types

// A custom general-purpose Partial
type MyPartial<T> = {
[K in keyof T]?: T[K];
};

// A custom general-purpose Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};

// Usage
interface Product {
id: number;
name: string;
price: number;
}

type PartialProduct = MyPartial<Product>;
// { id?: number; name?: string; price?: number; }

type ReadonlyProduct = MyReadonly<Product>;
// { readonly id: number; readonly name: string; readonly price: number; }

Adding and removing modifiers

In Mapped Types, you can remove modifiers using the - (minus) sign.

Removing readonly

interface ReadonlyUser {
readonly id: number;
readonly name: string;
readonly email: string;
}

// A type that removes readonly
// -readonly removes the readonly modifier
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};

type MutableUser = Mutable<ReadonlyUser>;
// Result:
// {
// id: number; // readonly was removed
// name: string;
// email: string;
// }

Removing optional

interface PartialUser {
id?: number;
name?: string;
email?: string;
}

// A type that removes optional (?)
// -? removes the optional modifier
type Concrete<T> = {
[K in keyof T]-?: T[K];
};

type ConcreteUser = Concrete<PartialUser>;
// Result:
// {
// id: number; // ? was removed
// name: string;
// email: string;
// }

Removing both modifiers

interface User {
readonly id?: number;
readonly name?: string;
readonly email?: string;
}

// Remove both readonly and ?
type MutableConcrete<T> = {
-readonly [K in keyof T]-?: T[K];
};

type FullUser = MutableConcrete<User>;
// Result:
// {
// id: number;
// name: string;
// email: string;
// }

Transforming the value type

You can also transform the property type itself.

interface User {
id: number;
name: string;
email: string;
}

// Transform all properties to the boolean type
type Booleanify<T> = {
[K in keyof T]: boolean;
};

type UserFlags = Booleanify<User>;
// Result:
// {
// id: boolean;
// name: boolean;
// email: boolean;
// }

// Wrap all properties in a Promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};

type AsyncUser = Promisify<User>;
// Result:
// {
// id: Promise<number>;
// name: Promise<string>;
// email: Promise<string>;
// }

// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};

type NullableUser = Nullable<User>;
// Result:
// {
// id: number | null;
// name: string | null;
// email: string | null;
// }

What are Conditional Types

Conditional Types are a "type-level if statement." They return a different type based on a condition.

Basic syntax

// Basic syntax: T extends U ? X : Y
// If T is assignable to type U, then X, otherwise Y

type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<"hello">; // true (a literal type is also a string)

A practical example: checking whether something is an array

type IsArray<T> = T extends any[] ? true : false;

type Test1 = IsArray<number[]>; // true
type Test2 = IsArray<string>; // false
type Test3 = IsArray<[1, 2, 3]>; // true (a tuple is also an array)

Transformation based on a type

// If it is an array, the element type; otherwise, the type itself
type Flatten<T> = T extends any[] ? T[number] : T;

type Test1 = Flatten<number[]>; // number
type Test2 = Flatten<string[]>; // string
type Test3 = Flatten<boolean>; // boolean

// Usage
function flatten<T>(value: T): Flatten<T> {
if (Array.isArray(value)) {
return value[0] as Flatten<T>;
}
return value as Flatten<T>;
}

The infer keyword

infer is a keyword for inferring and extracting a type within Conditional Types.

Getting the element type of an array

// Extract the element type of an array
// infer E infers the array's element type as E
type ElementType<T> = T extends (infer E)[] ? E : T;

type Test1 = ElementType<number[]>; // number
type Test2 = ElementType<string[]>; // string
type Test3 = ElementType<boolean>; // boolean (if not an array, the type itself)

Getting a function's return type (implementing ReturnType)

// A custom version of ReturnType
// (...args: any[]) => infer R infers the return type as R
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function createUser() {
return { id: 1, name: "Yamada Taro" };
}

type User = MyReturnType<typeof createUser>;
// Result: { id: number; name: string; }

Getting a function's parameter types (implementing Parameters)

// A custom version of Parameters
// (...args: infer P) infers the parameter types as P
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

function update(id: number, name: string, active: boolean): void {
console.log(id, name, active);
}

type UpdateParams = MyParameters<typeof update>;
// Result: [number, string, boolean]

Getting the value type of a Promise

// Take out the contents of a Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Test1 = UnwrapPromise<Promise<string>>; // string
type Test2 = UnwrapPromise<Promise<number>>; // number
type Test3 = UnwrapPromise<boolean>; // boolean

// Recursively unwrap deeply nested Promises
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U>
: T;

type Test4 = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>;
// Result: string

Distributive Conditional Types

When you apply Conditional Types to a union type, they are "distributed" over each member.

// Wrap each element in an array
type ToArray<T> = T extends any ? T[] : never;

type Test1 = ToArray<string | number>;
// Result: string[] | number[] (distributed over each element)

// To prevent distribution, wrap it in []
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type Test2 = ToArrayNonDistributive<string | number>;
// Result: (string | number)[] (the whole union becomes one array)

Why distribution happens

Distribution is a behavior that happens only for a naked type parameter (where T appears directly on the left side of extends). When you apply a conditional type to a union, it expands as follows.

// When T = string | number, ToArray<T> expands as follows
ToArray<string | number>
= (string extends any ? string[] : never) | (number extends any ? number[] : never)
= string[] | number[]

In other words, the conditional type is evaluated for each member of the union, and the results are joined with a union. This is "distribution."

info

When you want to suppress distribution, the standard technique is to wrap the type parameter in a single-element tuple like [T]. Wrapping it in a tuple makes T no longer a "naked type parameter", and the whole union is treated as a single type.

How to tell whether a type parameter is naked

PatternDistributed?
T extends U ? X : Y✅ Yes
[T] extends [U] ? X : Y❌ No
{ a: T } extends { a: U } ? X : Y❌ No
T & {} extends U ? X : Y❌ No

A practical example: implementing NonNullable

// Exclude null/undefined (distribution is leveraged)
type MyNonNullable<T> = T extends null | undefined ? never : T;

type Test = MyNonNullable<string | null | undefined>;
// Distribution behavior:
// MyNonNullable<string> | MyNonNullable<null> | MyNonNullable<undefined>
// = string | never | never
// = string

Template Literal Types

A feature for combining string literal types with template syntax.

Basic usage

type World = "world";
type Greeting = `hello ${World}`;
// Result: "hello world"

// Combining with union types
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";

type ColorSize = `${Color}-${Size}`;
// Result: "red-small" | "red-medium" | "red-large" |
// "blue-small" | "blue-medium" | "blue-large" |
// "green-small" | "green-medium" | "green-large"

Built-in string manipulation types

// Convert to uppercase
type Upper = Uppercase<"hello">; // "HELLO"

// Convert to lowercase
type Lower = Lowercase<"HELLO">; // "hello"

// Capitalize the first letter
type Cap = Capitalize<"hello">; // "Hello"

// Lowercase the first letter
type Uncap = Uncapitalize<"Hello">; // "hello"

A practical example: event handler types

type EventName = "click" | "focus" | "blur";

// Generate event handler names: on + capitalized first letter
type EventHandler = `on${Capitalize<EventName>}`;
// Result: "onClick" | "onFocus" | "onBlur"

// Generate the type of an event handler object
type EventHandlers = {
[K in EventName as `on${Capitalize<K>}`]: (event: Event) => void;
};
// Result:
// {
// onClick: (event: Event) => void;
// onFocus: (event: Event) => void;
// onBlur: (event: Event) => void;
// }

const handlers: EventHandlers = {
onClick: (e) => console.log("Clicked!"),
onFocus: (e) => console.log("Focused!"),
onBlur: (e) => console.log("Blurred!")
};

A practical example: CSS custom properties

type CSSProperty = "color" | "background" | "border";

// Generate CSS custom property names
type CSSVar = `--${CSSProperty}`;
// Result: "--color" | "--background" | "--border"

// A type-safe wrapper for getComputedStyle
function getCSSVar(element: Element, property: CSSVar): string {
return getComputedStyle(element).getPropertyValue(property);
}

infer × Template Literal Types

When you combine infer and Template Literal Types, you can extract a portion of a string and take it out as a type. This is a pattern for doing regex-like processing at the type level.

Extracting parameter names from a route path

// A type that extracts parameter names (":id" / ":postId") as a union type
// from a URL template like "/users/:id/posts/:postId"
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never

type UserPostParams = ExtractParams<'/users/:id/posts/:postId'>
// Result: 'id' | 'postId'

// A type-safe builder that receives parameters
type ParamsObject<Path extends string> = {
[K in ExtractParams<Path>]: string
}

function buildPath<Path extends string>(path: Path, params: ParamsObject<Path>): string {
return path.replace(/:(\w+)/g, (_, key) => (params as Record<string, string>)[key])
}

// You can pass parameters type-safely
buildPath('/users/:id/posts/:postId', { id: '1', postId: '42' })
// ✅ OK

buildPath('/users/:id/posts/:postId', { id: '1' })
// ❌ Error: postId is missing

snake_case → camelCase conversion

type SnakeToCamel<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
: S

type Result1 = SnakeToCamel<'hello_world'> // 'helloWorld'
type Result2 = SnakeToCamel<'user_first_name'> // 'userFirstName'

This pattern is practical for converting API responses (snake_case) to frontend (camelCase) types. By combining recursive conditional types with Template Literal Types, you can transform types without writing any runtime code.

info

TypeScript's type system has a limit on recursion depth (50 levels by default). A recursive type against an excessively deep nested structure causes an error, so it is important to split complex string processing between runtime functions and type-level processing.

Creating custom utility types

By combining the techniques you have learned, you can create your own utility types.

A type that generates getters

// Generate getter names from property names
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
name: string;
age: number;
}

type PersonGetters = Getters<Person>;
// Result:
// {
// getName: () => string;
// getAge: () => number;
// }

Extracting only properties of a specific type

// Extract only properties whose value is the string type
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface User {
id: number;
name: string;
email: string;
age: number;
}

type UserStrings = StringProperties<User>;
// Result:
// {
// name: string;
// email: string;
// }

Extracting only function properties

// Extract only properties whose value is a function type
type FunctionProperties<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};

interface Service {
name: string;
version: number;
start: () => void;
stop: () => void;
getStatus: () => string;
}

type ServiceMethods = FunctionProperties<Service>;
// Result:
// {
// start: () => void;
// stop: () => void;
// getStatus: () => string;
// }

Try it: create custom utility types ★★★

Implement custom utility types that meet the following requirements.

Requirements:

  1. DeepReadonly<T>: recursively make everything, including nested objects, read-only
  2. OptionalExcept<T, K>: make everything except the given properties K optional
  3. AsyncMethods<T>: wrap the return value of all methods in a Promise
// Implement the following
// type DeepReadonly<T> = ...
// type OptionalExcept<T, K> = ...
// type AsyncMethods<T> = ...
Hint
  1. DeepReadonly: apply Mapped Types recursively. Determine whether something is an object with Conditional Types
  2. OptionalExcept: combine Pick and Partial, apply Partial to keys other than the given ones, and make an intersection type
  3. AsyncMethods: determine whether something is a function type, and wrap the return value in a Promise
Answer and explanation
// 1. DeepReadonly: recursively make everything read-only
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K] // a function stays as is
: DeepReadonly<T[K]> // an object is recursed
: T[K]; // a primitive stays as is
};

// Test
interface NestedConfig {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
timeout: number;
}

type ReadonlyConfig = DeepReadonly<NestedConfig>;
// All nested properties become readonly

const config: ReadonlyConfig = {
server: {
host: "localhost",
port: 3000,
ssl: {
enabled: true,
cert: "cert.pem"
}
},
timeout: 5000
};

// config.server.ssl.enabled = false; // Error: read-only

// 2. OptionalExcept: make everything except the given properties optional
type OptionalExcept<T, K extends keyof T> =
Pick<T, K> & Partial<Omit<T, K>>;

// Test
interface User {
id: number;
name: string;
email: string;
age: number;
}

// id and name are required, everything else is optional
type UserWithRequiredIdName = OptionalExcept<User, "id" | "name">;

const user1: UserWithRequiredIdName = {
id: 1,
name: "Yamada Taro"
// email and age can be omitted
};

const user2: UserWithRequiredIdName = {
id: 2,
name: "Tanaka Hanako",
email: "tanaka@example.com" // it is OK to specify it
};

// 3. AsyncMethods: wrap method return values in a Promise
type AsyncMethods<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R> // if a function, wrap the return value in a Promise
: T[K]; // if not a function, stays as is
};

// Test
interface UserService {
name: string;
getUser(id: number): User;
saveUser(user: User): boolean;
deleteUser(id: number): void;
}

type AsyncUserService = AsyncMethods<UserService>;
// Result:
// {
// name: string; // not a function, so stays as is
// getUser(id: number): Promise<User>;
// saveUser(user: User): Promise<boolean>;
// deleteUser(id: number): Promise<void>;
// }

// An implementation example
const asyncService: AsyncUserService = {
name: "UserService",
async getUser(id: number) {
return { id, name: "User", email: "user@example.com", age: 20 };
},
async saveUser(user: User) {
console.log("Saving:", user);
return true;
},
async deleteUser(id: number) {
console.log("Deleting:", id);
}
};

Explanation:

  • DeepReadonly: recursively process things that are object but not Function with Conditional Types
  • OptionalExcept: take out the required properties with Pick<T, K>, make the rest optional with Partial<Omit<T, K>>, and compose them with an intersection type
  • AsyncMethods: extract the parameter and return types of a method with infer, and wrap the return value in a Promise

Once you understand these patterns, you can freely create project-specific utility types.

Summary

What you learned in this chapter:

  • Mapped Types: a "type-level for loop" that generates a new type from an existing one
  • Operating on modifiers: remove modifiers with -readonly or -?
  • Conditional Types: a "type-level if statement" with T extends U ? X : Y
  • The infer keyword: infer and extract a part of a type
  • Distributive Conditional Types: the distribution behavior over union types
  • Template Literal Types: template composition of string literal types
  • Built-in string manipulation types: Uppercase, Lowercase, Capitalize, Uncapitalize

By combining these advanced type features, you can create types similar to TypeScript's built-in utility types, as well as project-specific custom types.

Common pitfalls for beginners

warning

Common mistakes

❌ Forgetting keyof in Mapped Types

// ❌ No keyof
type BadPartial<T> = {
[K in T]?: T[K]; // Error: T cannot be used as a key
};

// ✅ Get the keys with keyof
type GoodPartial<T> = {
[K in keyof T]?: T[K];
};

Cause: With [K in T], T must be a set of keys, but an object type is not a set of keys. Solution: Get the set of keys (a union type) with keyof T, then iterate with Mapped Types.

❌ Not being mindful of Conditional Types distribution

type ToArray<T> = T extends any ? T[] : never;

// Distributed over a union type
type Result = ToArray<string | number>;
// Result: string[] | number[] (not (string | number)[])

// ❌ Using it without expecting distribution leads to an unexpected result

Cause: When you apply Conditional Types to a naked type parameter, they are distributed over each member of the union. Solution: To prevent distribution, wrap it in brackets like [T] extends [any].

❌ Trying to use a variable in Template Literal Types

// ❌ A runtime variable cannot be used in a type
const prefix = "on";
// type Handler = `${prefix}Click`; // Error

// ✅ Use a literal type
type Prefix = "on";
type Handler = `${Prefix}Click`; // "onClick"

Cause: Template Literal Types are types resolved at compile time, so runtime variables cannot be used. Solution: Make it a literal type with as const, or define it directly as a type.

❌ Getting the position of infer wrong

// ❌ infer is outside the condition
// type Bad<T> = infer R extends T ? R : never; // Error

// ✅ infer can only be used inside the condition
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

Cause: infer can only be used inside the extends clause of Conditional Types. Solution: Use it in the form T extends ... infer R ... ? ... : ....

❌ Forgetting the termination condition in a recursive type

// ❌ Without a termination condition, an infinite loop
// type BadFlatten<T> = T extends any[] ? BadFlatten<T[number]> : T;

// ✅ Set a termination condition
type Flatten<T> = T extends any[]
? T[number] extends any[]
? Flatten<T[number]>
: T[number]
: T;

Cause: A recursive type needs a termination condition. Solution: Define a base case (a condition that does not recurse any further) with Conditional Types.


Move on to the development environment. You will learn about modules, tsconfig, and building the development toolchain.