Skip to main content

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 key K in the keys of T...
  • : T[K] — ...the property type is the type of K in T.

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​

ModifierMeaning
?Make property optional
-?Make property required
readonlyMake property readonly
-readonlyMake 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​

  1. Write a MyPartial<T> that makes all properties optional.
  2. Write a MyReadonly<T> that makes all properties readonly.
  3. Write a Getters<T> that creates getX methods for each property.
  4. Write a Setters<T> that creates setX methods for each property.
  5. Write a FormState<T> that wraps each property in { value: T[K]; error: string | null }.
  6. Write a PickByType<T, V> that selects only properties whose type matches V.

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.