TypeScript — Mapped Types
Mapped types let you transform existing types by iterating over their keys and applying transformations to each property. They are the type-level equivalent of Array.map() — taking a type and producing a new type based on the same shape but with modified properties.
Every built-in utility type that changes props (Partial, Required, Readonly, Pick, Record) is implemented as a mapped type. With key remapping (TypeScript 4.1+), you can also rename keys, filter them, or generate entirely new property names.
note
The core syntax is {[P in K]: T} where K is a union of keys (typically keyof T) and T is the value type transformation. This iterates over each key P and applies the type expression.
| 1 | // Basic mapped type — make all values string |
| 2 | interface User { |
| 3 | id: number; |
| 4 | name: string; |
| 5 | age: number; |
| 6 | } |
| 7 | |
| 8 | type Stringify<T> = { |
| 9 | [P in keyof T]: string; |
| 10 | }; |
| 11 | |
| 12 | type StringUser = Stringify<User>; |
| 13 | // { id: string; name: string; age: string } |
| 14 | |
| 15 | // Homomorphic mapped type — preserves structure including modifiers |
| 16 | // A mapped type is homomorphic when it uses keyof T (or similar) |
| 17 | type Nullable<T> = { |
| 18 | [P in keyof T]: T[P] | null; |
| 19 | }; |
| 20 | |
| 21 | type NullableUser = Nullable<User>; |
| 22 | // { id: number | null; name: string | null; age: number | null } |
| 23 | |
| 24 | // Non-homomorphic — doesn't look at original type |
| 25 | type Wrapper<T> = { |
| 26 | [P in keyof T]: { value: T[P] }; |
| 27 | }; |
| 28 | |
| 29 | type WrappedUser = Wrapper<User>; |
| 30 | // { id: { value: number }; name: { value: string }; age: { value: number } } |
| 31 | |
| 32 | // Build-in equivalents implemented as mapped types: |
| 33 | type MyPartial<T> = { [P in keyof T]?: T[P] }; |
| 34 | type MyReadonly<T> = { readonly [P in keyof T]: T[P] }; |
| 35 | type MyRequired<T> = { [P in keyof T]-?: T[P] }; |
| 36 | |
| 37 | // Mapped type with generic constraint |
| 38 | type OnlyStrings<T extends Record<string, any>> = { |
| 39 | [K in keyof T]: T[K] extends string ? T[K] : never; |
| 40 | }[keyof T]; |
| 41 | |
| 42 | // Mapping over a union directly |
| 43 | type Box<T> = { |
| 44 | [K in keyof T]: { value: T[K] }; |
| 45 | }; |
| 46 | |
| 47 | type BoxedUser = Box<User>; |
info
TypeScript 4.1 introduced the as clause in mapped types, allowing you to rename, filter, or generate new keys. The syntax [K in keyof T as NewKey]: T[K] remaps each key K to NewKey. Use never to exclude a key.
| 1 | // Key remapping — prefix all keys |
| 2 | interface User { |
| 3 | name: string; |
| 4 | age: number; |
| 5 | email: string; |
| 6 | } |
| 7 | |
| 8 | type Prefixed<T, Prefix extends string> = { |
| 9 | [K in keyof T as `${Prefix}${string & K}`]: T[K]; |
| 10 | }; |
| 11 | |
| 12 | type PrefixedUser = Prefixed<User, "user_">; |
| 13 | // { user_name: string; user_age: number; user_email: string } |
| 14 | |
| 15 | // Key filtering — exclude by condition |
| 16 | type ExcludeByType<T, ExcludedType> = { |
| 17 | [K in keyof T as T[K] extends ExcludedType ? never : K]: T[K]; |
| 18 | }; |
| 19 | |
| 20 | type WithoutFunctions = ExcludeByType<User, Function>; |
| 21 | // Same as User (no functions in this case) |
| 22 | |
| 23 | interface Service { |
| 24 | start(): void; |
| 25 | stop(): void; |
| 26 | name: string; |
| 27 | version: number; |
| 28 | } |
| 29 | |
| 30 | type ServiceData = ExcludeByType<Service, Function>; |
| 31 | // { name: string; version: number } |
| 32 | |
| 33 | // Key filtering — keep only string values |
| 34 | type StringValuesOnly<T> = { |
| 35 | [K in keyof T as T[K] extends string ? K : never]: T[K]; |
| 36 | }; |
| 37 | |
| 38 | type UserStrings = StringValuesOnly<User>; |
| 39 | // { name: string; email: string } |
| 40 | |
| 41 | // Transform keys to uppercase |
| 42 | type UppercaseKeys<T> = { |
| 43 | [K in keyof T as Uppercase<string & K>]: T[K]; |
| 44 | }; |
| 45 | |
| 46 | type UpperUser = UppercaseKeys<User>; |
| 47 | // { NAME: string; AGE: number; EMAIL: string } |
| 48 | |
| 49 | // Generate getters from property names |
| 50 | type Getters<T> = { |
| 51 | [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; |
| 52 | }; |
| 53 | |
| 54 | type UserGetters = Getters<User>; |
| 55 | // { getName: () => string; getAge: () => number; getEmail: () => string } |
| 56 | |
| 57 | // Setters from property names |
| 58 | type Setters<T> = { |
| 59 | [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void; |
| 60 | }; |
| 61 | |
| 62 | type UserSetters = Setters<User>; |
| 63 | // { setName: (value: string) => void; setAge: (value: number) => void; ... } |
| 64 | |
| 65 | // Combined getters and setters |
| 66 | type FullAccessors<T> = Getters<T> & Setters<T>; |
best practice
Mapped types can add or remove the readonly and optional (?) modifiers using the + and - prefix syntax. +readonly adds readonly; -readonly removes it. +? makes optional; -? makes required.
| 1 | // Adding and removing readonly |
| 2 | type CreateReadonly<T> = { |
| 3 | +readonly [P in keyof T]: T[P]; |
| 4 | }; |
| 5 | |
| 6 | type RemoveReadonly<T> = { |
| 7 | -readonly [P in keyof T]: T[P]; |
| 8 | }; |
| 9 | |
| 10 | interface Config { |
| 11 | readonly host: string; |
| 12 | readonly port: number; |
| 13 | timeout: number; |
| 14 | } |
| 15 | |
| 16 | type MutableConfig = RemoveReadonly<Config>; |
| 17 | // { host: string; port: number; timeout: number } |
| 18 | |
| 19 | type DoubleReadonly = CreateReadonly<MutableConfig>; |
| 20 | // { readonly host: string; readonly port: number; readonly timeout: number } |
| 21 | |
| 22 | // Adding and removing optional |
| 23 | type MakeOptional<T> = { |
| 24 | [P in keyof T]+?: T[P]; |
| 25 | }; |
| 26 | |
| 27 | type MakeRequired<T> = { |
| 28 | [P in keyof T]-?: T[P]; |
| 29 | }; |
| 30 | |
| 31 | interface Options { |
| 32 | required: string; |
| 33 | optional?: number; |
| 34 | } |
| 35 | |
| 36 | type AllOptional = MakeOptional<Options>; |
| 37 | // { required?: string; optional?: number } |
| 38 | |
| 39 | type AllRequired = MakeRequired<Options>; |
| 40 | // { required: string; optional: number } |
| 41 | |
| 42 | // Default behavior (no + or -) |
| 43 | // [P in keyof T]?: — adds optional (same as +?) |
| 44 | // [P in keyof T]-?: — removes optional |
| 45 | // [P in keyof T] — preserves original optionality |
| 46 | |
| 47 | // Combined — make all properties mutable + required |
| 48 | type MutableAndRequired<T> = { |
| 49 | -readonly [P in keyof T]-?: T[P]; |
| 50 | }; |
| 51 | |
| 52 | interface ImmutableConfig { |
| 53 | readonly id: string; |
| 54 | readonly createdAt: Date; |
| 55 | name?: string; |
| 56 | } |
| 57 | |
| 58 | type WritableConfig = MutableAndRequired<ImmutableConfig>; |
| 59 | // { id: string; createdAt: Date; name: string } |
| 60 | |
| 61 | // Practical: clone a type but remove readonly |
| 62 | type Clone<T> = { |
| 63 | -readonly [P in keyof T]: T[P]; |
| 64 | }; |
| 65 | |
| 66 | // Practical: make the entire state writable (for reducers etc.) |
| 67 | type Writable<T> = { |
| 68 | -readonly [P in keyof T]: T[P]; |
| 69 | }; |
info
The + and - operators can be applied to both readonly and optional (?) independently. You can combine them in a single mapped type for precise control.
| 1 | // All combinations: |
| 2 | // +readonly, -readonly, readonly (same as +readonly) |
| 3 | // +?, -?, ? (same as +?) |
| 4 | |
| 5 | interface Original { |
| 6 | readonly a: string; |
| 7 | b?: number; |
| 8 | c: boolean; |
| 9 | } |
| 10 | |
| 11 | // Remove readonly, keep optionality as-is |
| 12 | type M1 = { -readonly [P in keyof Original]: Original[P] }; |
| 13 | // { a: string; b?: number; c: boolean } |
| 14 | |
| 15 | // Make all optional, keep readonly as-is |
| 16 | type M2 = { [P in keyof Original]+?: Original[P] }; |
| 17 | // { readonly a?: string; b?: number; c?: boolean } |
| 18 | |
| 19 | // Remove readonly AND make all required |
| 20 | type M3 = { -readonly [P in keyof Original]-?: Original[P] }; |
| 21 | // { a: string; b: number; c: boolean } |
| 22 | |
| 23 | // Make all readonly AND optional |
| 24 | type M4 = { +readonly [P in keyof Original]+?: Original[P] }; |
| 25 | // { readonly a?: string; readonly b?: number; readonly c?: boolean } |
| 26 | |
| 27 | // Practical: convert API response to writable form |
| 28 | interface APIResponse { |
| 29 | readonly id: string; |
| 30 | readonly created_at: string; |
| 31 | data: Record<string, unknown>; |
| 32 | readonly status: "ok" | "error"; |
| 33 | } |
| 34 | |
| 35 | // For mocking/testing, remove readonly |
| 36 | type MockResponse = { |
| 37 | -readonly [P in keyof APIResponse]: APIResponse[P]; |
| 38 | }; |
| 39 | |
| 40 | // Practical: use with as clause for full control |
| 41 | type Configurator<T> = { |
| 42 | -readonly [K in keyof T as T[K] extends Function ? never : K]-?: T[K]; |
| 43 | }; |
| 44 | |
| 45 | // Removes readonly, removes optional, excludes functions |
| 46 | interface Mixed { |
| 47 | readonly id: string; |
| 48 | name?: string; |
| 49 | execute?(): void; |
| 50 | } |
| 51 | |
| 52 | type Clean = Configurator<Mixed>; |
| 53 | // { id: string; name: string } — functions excluded, modifiers removed |
warning
These patterns combine mapped types with conditional types, key remapping, and template literals to solve common problems in real codebases.
| 1 | // Pattern 1: GettersToSetters — convert getter methods to setter methods |
| 2 | type GettersToSetters<T> = { |
| 3 | [K in keyof T as K extends `get${infer R}` |
| 4 | ? `set${R}` |
| 5 | : never |
| 6 | ]: T[K] extends () => infer V ? (value: V) => void : never; |
| 7 | }; |
| 8 | |
| 9 | interface UserAPI { |
| 10 | getName(): string; |
| 11 | getAge(): number; |
| 12 | getEmail(): string; |
| 13 | } |
| 14 | |
| 15 | type UserSettersAPI = GettersToSetters<UserAPI>; |
| 16 | // { setName: (value: string) => void; setAge: (value: number) => void; |
| 17 | // setEmail: (value: string) => void } |
| 18 | |
| 19 | // Pattern 2: NullableProperties — make specific properties nullable |
| 20 | type NullableProperties<T, K extends keyof T> = { |
| 21 | [P in keyof T]: P extends K ? T[P] | null : T[P]; |
| 22 | }; |
| 23 | |
| 24 | interface Order { |
| 25 | id: number; |
| 26 | shippedAt: Date; |
| 27 | deliveredAt: Date; |
| 28 | } |
| 29 | |
| 30 | type PendingOrder = NullableProperties<Order, "shippedAt" | "deliveredAt">; |
| 31 | // { id: number; shippedAt: Date | null; deliveredAt: Date | null } |
| 32 | |
| 33 | // Pattern 3: Writable — remove readonly deeply |
| 34 | type Writable<T> = { |
| 35 | -readonly [P in keyof T]: T[P] extends object |
| 36 | ? T[P] extends Function |
| 37 | ? T[P] |
| 38 | : Writable<T[P]> |
| 39 | : T[P]; |
| 40 | }; |
| 41 | |
| 42 | // Pattern 4: PickByValue — pick properties by their value type |
| 43 | type PickByValue<T, ValueType> = { |
| 44 | [K in keyof T as T[K] extends ValueType ? K : never]: T[K]; |
| 45 | }; |
| 46 | |
| 47 | interface Entity { |
| 48 | id: number; |
| 49 | name: string; |
| 50 | email: string; |
| 51 | status: boolean; |
| 52 | } |
| 53 | |
| 54 | type StringFields = PickByValue<Entity, string>; |
| 55 | // { name: string; email: string } |
| 56 | |
| 57 | type NumberFields = PickByValue<Entity, number>; |
| 58 | // { id: number } |
| 59 | |
| 60 | // Pattern 5: OmitByValue — omit properties by value type |
| 61 | type OmitByValue<T, ValueType> = { |
| 62 | [K in keyof T as T[K] extends ValueType ? never : K]: T[K]; |
| 63 | }; |
| 64 | |
| 65 | type NonStringFields = OmitByValue<Entity, string>; |
| 66 | // { id: number; status: boolean } |
| 67 | |
| 68 | // Pattern 6: ScreamingSnakeCase — convert all keys to SCREAMING_SNAKE_CASE |
| 69 | type ScreamingSnakeKeys<T> = { |
| 70 | [K in keyof T as Uppercase<string & K> extends string |
| 71 | ? Uppercase<string & K> |
| 72 | : never |
| 73 | ]: T[K]; |
| 74 | }; |
| 75 | |
| 76 | // Pattern 7: PrefixKeys — add a prefix to all keys |
| 77 | type PrefixKeys<T, P extends string> = { |
| 78 | [K in keyof T as `${P}${string & K}`]: T[K]; |
| 79 | }; |
| 1 | // Pattern 8: EventEmitter style — generate event map from event names |
| 2 | type EventMap<T extends string> = { |
| 3 | [K in T as `on${Capitalize<K>}`]: (handler: (event: K) => void) => void; |
| 4 | } & { |
| 5 | [K in T as `emit${Capitalize<K>}`]: (payload?: unknown) => void; |
| 6 | }; |
| 7 | |
| 8 | type MyEvents = EventMap<"click" | "focus" | "blur">; |
| 9 | // { onClick: (handler: (event: "click") => void) => void; |
| 10 | // onFocus: (handler: (event: "focus") => void) => void; |
| 11 | // onBlur: (handler: (event: "blur") => void) => void; |
| 12 | // emitClick: (payload?: unknown) => void; |
| 13 | // emitFocus: (payload?: unknown) => void; |
| 14 | // emitBlur: (payload?: unknown) => void } |
| 15 | |
| 16 | // Pattern 9: Nested paths with value types |
| 17 | type PathValues<T, Prefix extends string = ""> = { |
| 18 | [K in keyof T]-?: T[K] extends Record<string, any> |
| 19 | ? PathValues<T[K], `${Prefix}${string & K}.`> |
| 20 | : Record<`${Prefix}${string & K}`, T[K]>; |
| 21 | }[keyof T]; |
| 22 | |
| 23 | type DeepConfig = { |
| 24 | app: { name: string; debug: boolean }; |
| 25 | db: { host: string; port: number }; |
| 26 | }; |
| 27 | |
| 28 | type FlatConfig = PathValues<DeepConfig>; |
| 29 | // { "app.name": string } | { "app.debug": boolean } |
| 30 | // | { "db.host": string } | { "db.port": number } |
| 31 | |
| 32 | // Pattern 10: UnionToIntersection via mapped type trick |
| 33 | type UnionToIntersection<T> = ( |
| 34 | T extends any ? (x: T) => void : never |
| 35 | ) extends (x: infer R) => void |
| 36 | ? R |
| 37 | : never; |
| 38 | |
| 39 | // Pattern 11: FunctionPropertyNames — get method names only |
| 40 | type FunctionPropertyNames<T> = { |
| 41 | [K in keyof T]: T[K] extends Function ? K : never; |
| 42 | }[keyof T]; |
| 43 | |
| 44 | // Pattern 12: ModelToDto — strip methods, keep data |
| 45 | type DTO<T> = { |
| 46 | [K in keyof T as T[K] extends Function ? never : K]: T[K]; |
| 47 | }; |
best practice
Mapped types become exponentially more powerful when parameterized with generics. You can create flexible type transformers that adapt to any input shape.
| 1 | // Generic mapped type — works with any object |
| 2 | type PickByValueType<T, V> = { |
| 3 | [K in keyof T as T[K] extends V ? K : never]: T[K]; |
| 4 | }; |
| 5 | |
| 6 | // Usage with different types |
| 7 | interface Doc { |
| 8 | id: number; |
| 9 | title: string; |
| 10 | body: string; |
| 11 | published: boolean; |
| 12 | views: number; |
| 13 | } |
| 14 | |
| 15 | type StringProps = PickByValueType<Doc, string>; |
| 16 | // { title: string; body: string } |
| 17 | |
| 18 | type NumberProps = PickByValueType<Doc, number>; |
| 19 | // { id: number; views: number } |
| 20 | |
| 21 | type BoolProps = PickByValueType<Doc, boolean>; |
| 22 | // { published: boolean } |
| 23 | |
| 24 | // Generic with constraint |
| 25 | type Optionalize<T, K extends keyof T> = { |
| 26 | [P in keyof T]: P extends K ? T[P] | undefined : T[P]; |
| 27 | }; |
| 28 | |
| 29 | type UserWithOptionalEmail = Optionalize<User, "email">; |
| 30 | |
| 31 | // Two generic parameters for transformation |
| 32 | type RenameKeys<T, NameMap extends Record<string, string>> = { |
| 33 | [K in keyof T as K extends keyof NameMap ? NameMap[K] : K]: T[K]; |
| 34 | }; |
| 35 | |
| 36 | type UserRenamed = RenameKeys<User, { name: "username"; email: "emailAddress" }>; |
| 37 | // { username: string; age: number; emailAddress: string } |
| 38 | |
| 39 | // Generic mapped type with default |
| 40 | type WithMetadata<T, M = Record<string, unknown>> = { |
| 41 | data: T; |
| 42 | metadata: M; |
| 43 | }; |
| 44 | |
| 45 | type UserWithMeta = WithMetadata<User>; |
| 46 | // { data: User; metadata: Record<string, unknown> } |
| 47 | |
| 48 | // Nested generic mapped type |
| 49 | type DeepMap<T> = { |
| 50 | [P in keyof T]: T[P] extends Record<string, any> |
| 51 | ? DeepMap<T[P]> & { _type: string } |
| 52 | : T[P]; |
| 53 | }; |
info
Utility types are pre-built mapped types. Understanding the relationship helps you know when to use a utility vs when to write a custom mapped type. Every utility is a useful abstraction over a common mapped type pattern.
| 1 | // Utility Type ≡ Equivalent Mapped Type |
| 2 | // ───────────────────────────────────────────────── |
| 3 | // Partial<T> ≡ { [P in keyof T]+?: T[P] } |
| 4 | // Required<T> ≡ { [P in keyof T]-?: T[P] } |
| 5 | // Readonly<T> ≡ { +readonly [P in keyof T]: T[P] } |
| 6 | // Pick<T, K> ≡ { [P in K]: T[P] } |
| 7 | // Record<K, V> ≡ { [P in K]: V } |
| 8 | |
| 9 | // Partial<T> implementation |
| 10 | type MyPartial<T> = { |
| 11 | [P in keyof T]?: T[P]; |
| 12 | }; |
| 13 | |
| 14 | // Pick<T, K> implementation |
| 15 | type MyPick<T, K extends keyof T> = { |
| 16 | [P in K]: T[P]; |
| 17 | }; |
| 18 | |
| 19 | // Record<K, V> implementation |
| 20 | type MyRecord<K extends keyof any, V> = { |
| 21 | [P in K]: V; |
| 22 | }; |
| 23 | |
| 24 | // When to use utility vs custom mapped type: |
| 25 | // - Use utility: standard single transformation (Partial, Pick) |
| 26 | // - Use mapped: custom logic (filtering, renaming, conditional transforms) |
| 27 | // - Use combined: utility + mapped for complex needs |
| 28 | |
| 29 | // Example: Partial but only for specific keys |
| 30 | type PartialExcept<T, K extends keyof T> = { |
| 31 | [P in keyof T]: P extends K ? T[P] : T[P] | undefined; |
| 32 | } & Omit<T, K>; |
| 33 | |
| 34 | // Example: Readonly for object, mutable for functions |
| 35 | type ReadonlyData<T> = { |
| 36 | readonly [P in keyof T]: T[P] extends Function ? T[P] : T[P]; |
| 37 | }; |
| 38 | |
| 39 | // When you need something a utility doesn't provide: |
| 40 | // 1. Key transformation → custom with "as" clause |
| 41 | // 2. Filtering properties → custom with conditional + never |
| 42 | // 3. Value transformation → custom with conditional type |
| 43 | // 4. Deep transformations → custom recursive mapped type |
best practice
1. Use homomorphic mapped types (keyof T) when you want to preserve original modifiers. Use non-homomorphic (in keyof K) when building from scratch.
2. Leverage the as clause for key remapping instead of complex intersection types — it produces cleaner, more readable types.
3. Use never in the as clause to filter out keys. This is more explicit than using conditional types on the value side.
4. Combine -readonly and -? when creating mutable, concrete types from immutable interfaces.
5. Name your mapped types descriptively. Getters<T> immediately conveys intent; Transform<T, U> does not.
6. For deep transformations, ensure recursive mapped types have a base case to avoid excessive instantiation depth.
7. Prefer PickByValue and OmitByValue patterns over manually listing keys when the selection logic is type-based.
8. Remember that mapped types with as can generate any number of keys — including zero. Use this for conditional type inclusion/exclusion.