By the end of this chapter, you'll be able to transform any object type into any other object type with a single line of type code. Want to make every property optional? One line. Want to make every property readonly? One line. Want to rename every property from snake_case to camelCase? A few lines, but still — at the type level, at compile time, with full type safety.
Mapped types are TypeScript's type transformer. They iterate over the properties of a type and produce a new type. They're the foundation of Partial, Required, Readonly, Pick, Omit, and every other object utility type. And once you understand them, you can build your own.
The Syntax: Iterating Over Properties
A mapped type looks like this:
type MyMappedType<T> = {
[K in keyof T]: T[K];
};
Let's break it down:
[K in keyof T]— For each keyKin the keys ofT...: T[K]— ...the property type is the type ofKinT.
This is the identity mapped type — it produces a type identical to T. Not useful yet, but it shows the structure.
Making Everything Optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type PartialUser = MyPartial<User>;
// { name?: string; age?: number; email?: string; }
The ? after [K in keyof T] makes every property optional. This is exactly how TypeScript's built-in Partial<T> works.
Making Everything Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type ReadonlyUser = MyReadonly<User>;
// { readonly name: string; readonly age: number; readonly email: string; }
The readonly modifier before [K in keyof T] makes every property readonly.
Making Everything Required
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
type RequiredUser = MyRequired<Partial<User>>;
// { name: string; age: number; email: string; }
The -? removes the optional modifier. You can also use -readonly to remove readonly:
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
The Modifier Operators
| Modifier | Meaning |
|---|---|
? | Make property optional |
-? | Make property required |
readonly | Make property readonly |
-readonly | Make property mutable |
| (no modifier) | Keep the original modifier |
Key Remapping with as
TypeScript lets you transform keys while mapping:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
The as clause transforms each key. K is the original key. as `get${Capitalize<string & K>} produces the new key. The string & K cast is needed because K could be string | number | symbol — Capitalize only works on string.
Filtering Keys with as
You can use as to filter which keys are included:
// Only include string properties
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
name: string;
age: number;
email: string;
isAdmin: boolean;
}
type UserStrings = StringProperties<User>;
// { name: string; email: string; }
When the as clause returns never, the property is excluded. This is how you filter properties in a mapped type.
Excluding Specific Keys
type Without<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type UserWithoutEmail = Without<User, "email">;
// { name: string; age: number; isAdmin: boolean; }
Combining Mapped Types with Template Literal Types
The real power comes from combining mapped types with template literal types:
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (event: T[K]) => void;
};
type Events = {
click: { x: number; y: number };
focus: { target: string };
blur: { target: string };
};
type Handlers = EventHandlers<Events>;
// {
// onClick: (event: { x: number; y: number }) => void;
// onFocus: (event: { target: string }) => void;
// onBlur: (event: { target: string }) => void;
// }
Transforming Property Types
Mapped types can transform the types of properties, not just their modifiers:
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null; email: string | null; }
type DeepNullable<T> = {
[K in keyof T]: T[K] extends object ? DeepNullable<T[K]> | null : T[K] | null;
};
Wrapping Properties
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
type AsyncUser = Promisify<User>;
// { name: Promise<string>; age: Promise<number>; email: Promise<string>; }
Real-World Mapped Type Patterns
Pattern 1: Form State
type FormState<T> = {
[K in keyof T]: {
value: T[K];
touched: boolean;
error: string | null;
};
};
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
type LoginFormState = FormState<LoginForm>;
// {
// email: { value: string; touched: boolean; error: string | null };
// password: { value: string; touched: boolean; error: string | null };
// rememberMe: { value: boolean; touched: boolean; error: string | null };
// }
Pattern 2: API Response Wrapper
type ApiWrapper<T> = {
[K in keyof T]: {
data: T[K];
loading: boolean;
error: Error | null;
lastFetched: Date | null;
};
};
Pattern 3: Selectors from State
type Selectors<T> = {
[K in keyof T as `select${Capitalize<string & K>}`]: (state: T) => T[K];
};
interface AppState {
user: User | null;
theme: "light" | "dark";
notifications: Notification[];
}
type AppSelectors = Selectors<AppState>;
// {
// selectUser: (state: AppState) => User | null;
// selectTheme: (state: AppState) => "light" | "dark";
// selectNotifications: (state: AppState) => Notification[];
// }
Pattern 4: Validation Schema
type ValidationSchema<T> = {
[K in keyof T]: (value: T[K]) => string | null;
};
interface UserInput {
name: string;
age: number;
email: string;
}
type UserValidation = ValidationSchema<UserInput>;
// {
// name: (value: string) => string | null;
// age: (value: number) => string | null;
// email: (value: string) => string | null;
// }
Mapped Types with Tuples
Mapped types work on tuples too:
type MapTuple<T extends unknown[]> = {
[K in keyof T]: Promise<T[K]>;
};
type Promises = MapTuple<[string, number, boolean]>;
// [Promise<string>, Promise<number>, Promise<boolean>]
The Built-in Mapped Types (How They Work)
Now you can understand how TypeScript's standard utility types are implemented:
type Partial<T> = {
[K in keyof T]?: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Record<K extends keyof unknown, V> = {
[P in K]: V;
};
Omit is slightly different — it uses as to exclude keys:
type Omit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
Try This: Mapped Types
- Write a
MyPartial<T>that makes all properties optional. - Write a
MyReadonly<T>that makes all properties readonly. - Write a
Getters<T>that createsgetXmethods for each property. - Write a
Setters<T>that createssetXmethods for each property. - Write a
FormState<T>that wraps each property in{ value: T[K]; error: string | null }. - Write a
PickByType<T, V>that selects only properties whose type matchesV.
Time needed: 25 minutes.
What to notice: How mapped types let you transform entire object types in a single expression. How the as clause gives you key-level control. How combining mapped types with template literal types unlocks patterns like onClick handlers and getX/setX accessors.
The Question
You now know how to transform object types with mapped types. But there's another kind of type transformation: conditional types. Types that make decisions. Types that say "if this type is a string, do X; if it's a number, do Y."
You've already seen conditional types with infer. Now it's time to understand them fully — including distributive conditional types, the never trick, and how to build type-level decision trees.
In the next chapter: conditional types in depth, distributive conditional types, the never trick, and how to build types that make complex decisions based on their inputs.