|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/mapped
$cat docs/typescript-—-mapped-types.md
updated Recently·12 min read·published

TypeScript — Mapped Types

TypeScriptAdvanced
Introduction

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

Mapped types are purely compile-time. They have no runtime representation — the output type is what TypeScript uses for type checking while the runtime JavaScript is unaffected.
Basic Mapped Type Syntax

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.

basic-mapped.ts
TypeScript
1// Basic mapped type — make all values string
2interface User {
3 id: number;
4 name: string;
5 age: number;
6}
7
8type Stringify<T> = {
9 [P in keyof T]: string;
10};
11
12type 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)
17type Nullable<T> = {
18 [P in keyof T]: T[P] | null;
19};
20
21type NullableUser = Nullable<User>;
22// { id: number | null; name: string | null; age: number | null }
23
24// Non-homomorphic — doesn't look at original type
25type Wrapper<T> = {
26 [P in keyof T]: { value: T[P] };
27};
28
29type WrappedUser = Wrapper<User>;
30// { id: { value: number }; name: { value: string }; age: { value: number } }
31
32// Build-in equivalents implemented as mapped types:
33type MyPartial<T> = { [P in keyof T]?: T[P] };
34type MyReadonly<T> = { readonly [P in keyof T]: T[P] };
35type MyRequired<T> = { [P in keyof T]-?: T[P] };
36
37// Mapped type with generic constraint
38type 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
43type Box<T> = {
44 [K in keyof T]: { value: T[K] };
45};
46
47type BoxedUser = Box<User>;

info

Homomorphic mapped types (those using keyof T) preserve readonly and optional (?) modifiers from the original type unless explicitly overridden. Non-homomorphic types start fresh.
Key Remapping (as clause)

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.

key-remapping.ts
TypeScript
1// Key remapping — prefix all keys
2interface User {
3 name: string;
4 age: number;
5 email: string;
6}
7
8type Prefixed<T, Prefix extends string> = {
9 [K in keyof T as `${Prefix}${string & K}`]: T[K];
10};
11
12type PrefixedUser = Prefixed<User, "user_">;
13// { user_name: string; user_age: number; user_email: string }
14
15// Key filtering — exclude by condition
16type ExcludeByType<T, ExcludedType> = {
17 [K in keyof T as T[K] extends ExcludedType ? never : K]: T[K];
18};
19
20type WithoutFunctions = ExcludeByType<User, Function>;
21// Same as User (no functions in this case)
22
23interface Service {
24 start(): void;
25 stop(): void;
26 name: string;
27 version: number;
28}
29
30type ServiceData = ExcludeByType<Service, Function>;
31// { name: string; version: number }
32
33// Key filtering — keep only string values
34type StringValuesOnly<T> = {
35 [K in keyof T as T[K] extends string ? K : never]: T[K];
36};
37
38type UserStrings = StringValuesOnly<User>;
39// { name: string; email: string }
40
41// Transform keys to uppercase
42type UppercaseKeys<T> = {
43 [K in keyof T as Uppercase<string & K>]: T[K];
44};
45
46type UpperUser = UppercaseKeys<User>;
47// { NAME: string; AGE: number; EMAIL: string }
48
49// Generate getters from property names
50type Getters<T> = {
51 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
52};
53
54type UserGetters = Getters<User>;
55// { getName: () => string; getAge: () => number; getEmail: () => string }
56
57// Setters from property names
58type Setters<T> = {
59 [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
60};
61
62type UserSetters = Setters<User>;
63// { setName: (value: string) => void; setAge: (value: number) => void; ... }
64
65// Combined getters and setters
66type FullAccessors<T> = Getters<T> & Setters<T>;

best practice

Key remapping with as is the most powerful mapped type feature. Use it to create type-safe ORM-like interfaces, generate event handler types from event names, and transform API response shapes. Combine with template literal types and string manipulation for maximum expressiveness.
Property Modifiers

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.

property-modifiers.ts
TypeScript
1// Adding and removing readonly
2type CreateReadonly<T> = {
3 +readonly [P in keyof T]: T[P];
4};
5
6type RemoveReadonly<T> = {
7 -readonly [P in keyof T]: T[P];
8};
9
10interface Config {
11 readonly host: string;
12 readonly port: number;
13 timeout: number;
14}
15
16type MutableConfig = RemoveReadonly<Config>;
17// { host: string; port: number; timeout: number }
18
19type DoubleReadonly = CreateReadonly<MutableConfig>;
20// { readonly host: string; readonly port: number; readonly timeout: number }
21
22// Adding and removing optional
23type MakeOptional<T> = {
24 [P in keyof T]+?: T[P];
25};
26
27type MakeRequired<T> = {
28 [P in keyof T]-?: T[P];
29};
30
31interface Options {
32 required: string;
33 optional?: number;
34}
35
36type AllOptional = MakeOptional<Options>;
37// { required?: string; optional?: number }
38
39type 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
48type MutableAndRequired<T> = {
49 -readonly [P in keyof T]-?: T[P];
50};
51
52interface ImmutableConfig {
53 readonly id: string;
54 readonly createdAt: Date;
55 name?: string;
56}
57
58type WritableConfig = MutableAndRequired<ImmutableConfig>;
59// { id: string; createdAt: Date; name: string }
60
61// Practical: clone a type but remove readonly
62type Clone<T> = {
63 -readonly [P in keyof T]: T[P];
64};
65
66// Practical: make the entire state writable (for reducers etc.)
67type Writable<T> = {
68 -readonly [P in keyof T]: T[P];
69};

info

The + prefix is optional — [P in keyof T]?: is same as +?. The - prefix is explicit and required for removing modifiers. -readonly and -? are the key patterns for creating mutable/required variants.
Adding & Removing Modifiers (+/-)

The + and - operators can be applied to both readonly and optional (?) independently. You can combine them in a single mapped type for precise control.

add-remove-modifiers.ts
TypeScript
1// All combinations:
2// +readonly, -readonly, readonly (same as +readonly)
3// +?, -?, ? (same as +?)
4
5interface Original {
6 readonly a: string;
7 b?: number;
8 c: boolean;
9}
10
11// Remove readonly, keep optionality as-is
12type M1 = { -readonly [P in keyof Original]: Original[P] };
13// { a: string; b?: number; c: boolean }
14
15// Make all optional, keep readonly as-is
16type M2 = { [P in keyof Original]+?: Original[P] };
17// { readonly a?: string; b?: number; c?: boolean }
18
19// Remove readonly AND make all required
20type M3 = { -readonly [P in keyof Original]-?: Original[P] };
21// { a: string; b: number; c: boolean }
22
23// Make all readonly AND optional
24type 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
28interface 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
36type MockResponse = {
37 -readonly [P in keyof APIResponse]: APIResponse[P];
38};
39
40// Practical: use with as clause for full control
41type 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
46interface Mixed {
47 readonly id: string;
48 name?: string;
49 execute?(): void;
50}
51
52type Clean = Configurator<Mixed>;
53// { id: string; name: string } — functions excluded, modifiers removed

warning

You cannot use + or - with non-homomorphic mapped types (where the key source is not keyof T). The modifiers only make sense when there's an original type to compare against.
Real-World Patterns

These patterns combine mapped types with conditional types, key remapping, and template literals to solve common problems in real codebases.

real-world-patterns.ts
TypeScript
1// Pattern 1: GettersToSetters — convert getter methods to setter methods
2type 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
9interface UserAPI {
10 getName(): string;
11 getAge(): number;
12 getEmail(): string;
13}
14
15type 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
20type NullableProperties<T, K extends keyof T> = {
21 [P in keyof T]: P extends K ? T[P] | null : T[P];
22};
23
24interface Order {
25 id: number;
26 shippedAt: Date;
27 deliveredAt: Date;
28}
29
30type PendingOrder = NullableProperties<Order, "shippedAt" | "deliveredAt">;
31// { id: number; shippedAt: Date | null; deliveredAt: Date | null }
32
33// Pattern 3: Writable — remove readonly deeply
34type 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
43type PickByValue<T, ValueType> = {
44 [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
45};
46
47interface Entity {
48 id: number;
49 name: string;
50 email: string;
51 status: boolean;
52}
53
54type StringFields = PickByValue<Entity, string>;
55// { name: string; email: string }
56
57type NumberFields = PickByValue<Entity, number>;
58// { id: number }
59
60// Pattern 5: OmitByValue — omit properties by value type
61type OmitByValue<T, ValueType> = {
62 [K in keyof T as T[K] extends ValueType ? never : K]: T[K];
63};
64
65type NonStringFields = OmitByValue<Entity, string>;
66// { id: number; status: boolean }
67
68// Pattern 6: ScreamingSnakeCase — convert all keys to SCREAMING_SNAKE_CASE
69type 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
77type PrefixKeys<T, P extends string> = {
78 [K in keyof T as `${P}${string & K}`]: T[K];
79};
real-world-patterns-2.ts
TypeScript
1// Pattern 8: EventEmitter style — generate event map from event names
2type 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
8type 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
17type 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
23type DeepConfig = {
24 app: { name: string; debug: boolean };
25 db: { host: string; port: number };
26};
27
28type 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
33type 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
40type 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
45type DTO<T> = {
46 [K in keyof T as T[K] extends Function ? never : K]: T[K];
47};

best practice

The most powerful patterns combine key remapping (as), conditional types for filtering, and template literal types for key transformation. Study the type-fest and ts-toolbelt libraries for battle-tested implementations of these patterns.
Mapped Types with Generics

Mapped types become exponentially more powerful when parameterized with generics. You can create flexible type transformers that adapt to any input shape.

mapped-with-generics.ts
TypeScript
1// Generic mapped type — works with any object
2type PickByValueType<T, V> = {
3 [K in keyof T as T[K] extends V ? K : never]: T[K];
4};
5
6// Usage with different types
7interface Doc {
8 id: number;
9 title: string;
10 body: string;
11 published: boolean;
12 views: number;
13}
14
15type StringProps = PickByValueType<Doc, string>;
16// { title: string; body: string }
17
18type NumberProps = PickByValueType<Doc, number>;
19// { id: number; views: number }
20
21type BoolProps = PickByValueType<Doc, boolean>;
22// { published: boolean }
23
24// Generic with constraint
25type Optionalize<T, K extends keyof T> = {
26 [P in keyof T]: P extends K ? T[P] | undefined : T[P];
27};
28
29type UserWithOptionalEmail = Optionalize<User, "email">;
30
31// Two generic parameters for transformation
32type RenameKeys<T, NameMap extends Record<string, string>> = {
33 [K in keyof T as K extends keyof NameMap ? NameMap[K] : K]: T[K];
34};
35
36type UserRenamed = RenameKeys<User, { name: "username"; email: "emailAddress" }>;
37// { username: string; age: number; emailAddress: string }
38
39// Generic mapped type with default
40type WithMetadata<T, M = Record<string, unknown>> = {
41 data: T;
42 metadata: M;
43};
44
45type UserWithMeta = WithMetadata<User>;
46// { data: User; metadata: Record<string, unknown> }
47
48// Nested generic mapped type
49type DeepMap<T> = {
50 [P in keyof T]: T[P] extends Record<string, any>
51 ? DeepMap<T[P]> & { _type: string }
52 : T[P];
53};

info

When writing generic mapped types, add constraints to your type parameters (T extends Record<string, any>) to get better error messages and autocomplete. Without constraints, TypeScript will error on types that don't have keyof.
Mapped Types vs Utility Types

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.

mapped-vs-utility.ts
TypeScript
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
10type MyPartial<T> = {
11 [P in keyof T]?: T[P];
12};
13
14// Pick<T, K> implementation
15type MyPick<T, K extends keyof T> = {
16 [P in K]: T[P];
17};
18
19// Record<K, V> implementation
20type 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
30type 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
35type 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

Reach for built-in utilities first — they communicate intent clearly. Write custom mapped types when you need key remapping, conditional filtering, or transformations that combine multiple modifiers. A well-named custom mapped type (Getters<T>, Writable<T>) can be more readable than a chain of built-ins.
Best Practices

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.

$Blueprint — Engineering Documentation·Section ID: TS-MAP·Revision: 1.0