TypeScript 30 🔷 Mapped Types
A mapped type transforms an existing type by iterating over its keys and producing a new type for each one. It’s how TypeScript’s built-in utilities like Partial<T>, Required<T>, Readonly<T>, and Pick<T, K> are implemented — and how you write your own transformations. Mapped types take the keys of one type, apply a rule to each, and produce a new type with the same or transformed keys. They’re the foundation of type-level programming that reshapes object types.
Key point: A mapped type uses [K in keyof T] to iterate over the keys of T. For each key, it produces a member — a property type, a modifier, or nothing. The result is a new object type. Modifiers — readonly, ?, and the - to remove them — control the shape of the new type. Once you can read and write mapped types, the utility types stop being magic and become patterns you can extend.
What a mapped type is
A mapped type iterates over the keys of a type and produces a new type.
type Partial<T> = {
[K in keyof T]?: T[K];
};
Reading this left to right:
[K in keyof T]— for each keyKofT?:— make the property optionalT[K]— use the type of the original property
Given User, Partial<User> makes every property optional.
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// {
// id?: number;
// name?: string;
// email?: string;
// }
The syntax looks like an index signature, but K in keyof T is a for-each over the keys, not an index of arbitrary strings.
What mapped types can do:
- Add or remove
readonly - Add or remove optionality (
?) - Change the value type
- Rename keys (via
as) - Filter keys (via
aswithnever) - Combine with conditional types
What they can’t do:
- Add keys not present in the source
- Change the type parameter name in the output (only via
as) - Operate on non-object types directly
Why mapped types matter: They’re how you derive a new type from an existing one without duplicating it. Change User, and Partial<User>, Readonly<User>, Pick<User, 'id'> all update. That’s single-source-of-truth applied to types.
Why “mapped”: The type maps over the keys, like
Array.mapmaps over elements. Each key becomes a property in the new type, with a rule applied. The syntax[K in ...]is the type-level equivalent offor— it iterates the union of keys.
The basic form
The general form is:
type Mapped<T> = {
[K in keyof T]: NewType;
};
Every property of T becomes a property of Mapped<T>, with type NewType.
Identity mapped type:
type Clone<T> = {
[K in keyof T]: T[K];
};
Clone<T> reproduces T exactly. Not useful by itself, but it’s the base for transformations.
Change value types:
type Stringify<T> = {
[K in keyof T]: string;
};
type UserStrings = Stringify<User>;
// {
// id: string;
// name: string;
// email: string;
// }
Every property becomes a string.
Wrap values in a type:
type Wrapped<T> = {
[K in keyof T]: { value: T[K] };
};
type WrappedUser = Wrapped<User>;
// {
// id: { value: number };
// name: { value: string };
// email: { value: string };
// }
Each property becomes { value: ... } holding the original type.
Iterate over a union of literal keys:
type Role = 'admin' | 'user' | 'guest';
type Permissions = {
[K in Role]: boolean;
};
// {
// admin: boolean;
// user: boolean;
// guest: boolean;
// }
K in Role iterates the union directly — no keyof needed. This is how you map over any union.
Why iterating a union works: K in X iterates whatever X is — a union of strings, a keyof T, anything that’s a union of literal types. That flexibility is what makes mapped types so general.
Why the syntax is unusual:
{ [K in X]: Y }looks like an index signature, but it’s not. It’s a type-level loop. TypeScript’s designers reused the bracket syntax because both forms iterate — one over runtime keys, one over type-level keys. Theinkeyword is the tell.
Modifiers — readonly and ?
Mapped types can add or remove readonly and ?.
Adding readonly:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
Adding optional:
type Partial<T> = {
[K in keyof T]?: T[K];
};
Removing readonly — the - modifier:
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface Frozen {
readonly id: number;
readonly name: string;
}
type Unfrozen = Mutable<Frozen>;
// {
// id: number;
// name: string;
// }
-readonly removes the modifier.
Removing optional — -?:
type Required<T> = {
[K in keyof T]-?: T[K];
};
interface Opt {
id?: number;
name?: string;
}
type Req = Required<Opt>;
// {
// id: number;
// name: string;
// }
-? removes optionality.
Combining both:
type RequiredMutable<T> = {
-readonly [K in keyof T]-?: T[K];
};
Removes both readonly and ? in one pass.
The + modifier: +readonly and +? are explicit forms of the default — rarely needed, but they document intent.
type ExplicitReadonly<T> = {
+readonly [K in keyof T]: T[K];
};
Modifiers reference:
| Modifier | Effect |
|---|---|
| (none) | Preserves existing modifier |
readonly | Adds readonly |
+readonly | Same as readonly |
-readonly | Removes readonly |
? | Adds optional |
+? | Same as ? |
-? | Removes optional |
Important: Without an explicit modifier, TypeScript preserves the source’s modifiers. { [K in keyof T]: T[K] } keeps readonly and ? from T. Adding readonly on top does nothing if the source is already readonly; -readonly removes it.
Why the
-modifier exists: TypeScript needed a way to express “remove this modifier.” A modifier prefix only added. The-inverts it —-readonlymeans “remove readonly,”-?means “remove optional.” It’s a small syntax that unlocksMutableandRequiredas the inverses ofReadonlyandPartial.
Key remapping with as
The as clause renames keys in the output.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
email: string;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getEmail: () => string;
// }
Each key becomes get + the capitalized original name. The values become functions returning the original type.
Anatomy of the remap:
K in keyof T— iterate the keysas \get${Capitalize<string & K>}“ — rename each key: () => T[K]— the new value type
Changing keys to a prefix:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
type Prefixed = Prefix<User, 'user_'>;
// {
// user_name: string;
// user_email: string;
// }
Changing keys to uppercase:
type UpperKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};
type Upper = UpperKeys<User>;
// {
// NAME: string;
// EMAIL: string;
// }
Removing keys with never: When as produces never, the key is dropped.
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
type NoNumbers = OmitByType<Mixed, number>;
// {
// name: string;
// active: boolean;
// }
as T[K] extends U ? never : K — if the property’s type matches U, the key becomes never (removed); otherwise it stays.
Filtering keys:
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type OnlyNumbers = PickByType<Mixed, number>;
// {
// id: number;
// count: number;
// }
The inverse — only keys whose value matches U survive.
Why as is powerful: It lets a mapped type transform keys, not just values. Prefix, suffix, case-convert, filter — anything you can express as a string operation or conditional. That’s how utilities like Getters, Setters, and OmitByType are built.
Why
neverremoves keys: In TypeScript, a property with typeneveris meaningless — no value can inhabit it. Whenasproducesnever, the compiler drops the property entirely. That’s the mechanism for key filtering: map unwanted keys tonever.
Mapping over unions
Mapped types work over any union of literal types, not just keyof T.
type Direction = 'north' | 'south' | 'east' | 'west';
type Labels = {
[K in Direction]: string;
};
// {
// north: string;
// south: string;
// east: string;
// west: string;
// }
Each union member becomes a key.
Building a lookup table:
type Status = 'idle' | 'loading' | 'ready' | 'error';
type Messages = {
[K in Status]: string;
};
const messages: Messages = {
idle: 'Waiting',
loading: 'Loading...',
ready: 'Ready',
error: 'Failed'
};
The mapped type forces every status to have a message. Adding a status breaks the constant until updated.
Mapping over keyof and unions together:
type EventMap = {
click: [x: number, y: number];
keydown: [key: string];
};
type Handler<T> = {
[K in keyof T]: (...args: T[K]) => void;
};
type Handlers = Handler<EventMap>;
// {
// click: (x: number, y: number) => void;
// keydown: (key: string) => void;
// }
Each event’s tuple type becomes the parameter list of a handler.
Why union mapping matters: It’s how you build exhaustive lookup tables, dispatchers, and typed event systems. The mapped type guarantees every key is present, and TypeScript flags any missing entry.
Why
K in Uis more general thanK in keyof T:keyof Tis a union of keys.K in Ufor any unionUis the same mechanism.keyof Tis just a special case whereUhappens to be the keys of a type. Understanding the general form lets you map over enums, literal unions, and any other union of strings or numbers.
Mapping modifiers and conditional types
Mapped types combine with conditional types to produce powerful transformations.
Optional only for certain value types:
type OptionalByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]?: T[K];
} & {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
type User2 = OptionalByType<User, number>;
// {
// id?: number;
// name: string;
// email: string;
// }
The first mapped type picks keys whose values are U and makes them optional; the second picks the rest and keeps them required.
Conditional value types:
type ReadonlyIfArray<T> = {
[K in keyof T]: T[K] extends readonly unknown[] ? readonly T[K] : T[K];
};
interface Config {
tags: string[];
name: string;
}
type ReadonlyTags = ReadonlyIfArray<Config>;
// {
// tags: readonly string[];
// name: string;
// }
Each property is conditionally transformed based on its type.
Removing optional and readonly together:
type Concrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
Adding both conditionally:
type Locked<T> = {
readonly [K in keyof T]-?: T[K];
};
Deep transformation — recursive mapped type:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface Nested {
user: { name: string; address: { city: string } };
count: number;
}
type FrozenNested = DeepReadonly<Nested>;
// All levels readonly
Each property is checked: if it’s an object, recurse; otherwise, keep it.
Why conditional mapped types matter: They let you apply different rules to different properties based on their types. Optional for numbers, required for strings. Deeply readonly for nested objects, unchanged for primitives. The combination of mapped types and conditional types is where TypeScript’s type-level power really shows.
Why recursion works in mapped types: A mapped type can reference itself in the value position.
DeepReadonly<T[K]>calls the mapped type on each property’s type. Since TypeScript resolves types lazily, the recursion terminates when the property is no longer an object.
A full example
A type-safe state management system with mapped types.
// ============================================
// STATE SHAPE
// ============================================
interface AppState {
user: { id: number; name: string };
posts: { id: number; title: string }[];
loading: boolean;
error: string | null;
}
// ============================================
// DERIVED TYPES
// ============================================
// Partial for patches
type StatePatch = Partial<AppState>;
// Readonly for selectors
type ReadonlyState = Readonly<AppState>;
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
// Pick a subset
type UISlice = Pick<AppState, 'loading' | 'error'>;
// Omit for reducer state
type DataState = Omit<AppState, 'loading' | 'error'>;
// Getters for selectors
type Selectors = {
[K in keyof AppState as `select${Capitalize<string & K>}`]: (
state: AppState
) => AppState[K];
};
// Setters for actions
type Setters = {
[K in keyof AppState as `set${Capitalize<string & K>}`]: (
state: AppState,
value: AppState[K]
) => AppState;
};
// ============================================
// IMPLEMENTATION
// ============================================
const initialState: AppState = {
user: { id: 1, name: 'Alice' },
posts: [],
loading: false,
error: null
};
const selectors: Selectors = {
selectUser: state => state.user,
selectPosts: state => state.posts,
selectLoading: state => state.loading,
selectError: state => state.error
};
const setters: Setters = {
setUser: (state, user) => ({ ...state, user }),
setPosts: (state, posts) => ({ ...state, posts }),
setLoading: (state, loading) => ({ ...state, loading }),
setError: (state, error) => ({ ...state, error })
};
// ============================================
// USAGE
// ============================================
let state: AppState = initialState;
// Selectors — typed by key
const user = selectors.selectUser(state);
// { id: number; name: string }
const loading = selectors.selectLoading(state);
// boolean
// Setters — typed by key and value
state = setters.setUser(state, { id: 2, name: 'Bob' });
state = setters.setLoading(state, true);
// Wrong value type fails
// setters.setUser(state, 'not a user'); // ❌
// setters.setLoading(state, 42); // ❌
// Patches — partial state
const patch: StatePatch = { loading: false };
state = { ...state, ...patch };
console.log(user);
console.log(loading);
console.log(state);
What this shows:
Partial<AppState>— every property optionalReadonly<AppState>— every property readonlyDeepReadonly<T>— recursive readonlyPickandOmit— subsets of propertiesSelectors— mapped type renaming keys toselect*methodsSetters— mapped type renaming keys toset*methods
Every derived type updates when AppState changes. Add a property, and every utility picks it up.
Why this shape: It’s how real state management is typed. Selectors and setters are generated from the state shape. Change the state, and the compiler tells you what needs updating. Mapped types make the whole pattern possible without duplication.
Complete Example Session
# ============================================
# PART 1: BASIC MAPPED TYPE
# ============================================
cat > basic.ts << 'EOF'
interface User {
id: number;
name: string;
}
type Clone<T> = { [K in keyof T]: T[K] };
const u: Clone<User> = { id: 1, name: 'Alice' };
console.log(u);
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: UTILITY TYPES
# ============================================
cat > utils.ts << 'EOF'
interface User {
id: number;
name: string;
email: string;
}
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyMutable<T> = { -readonly [K in keyof T]: T[K] };
const p: MyPartial<User> = { name: 'Alice' };
const r: MyReadonly<User> = { id: 1, name: 'Alice', email: 'a@b.c' };
// r.id = 2; // ❌
console.log(p, r);
EOF
npx tsc --noEmit utils.ts
# (no errors)
# ============================================
# PART 3: KEY REMAPPING
# ============================================
cat > remap.ts << 'EOF'
interface User {
name: string;
email: string;
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getName: () => string; getEmail: () => string }
const g: UserGetters = {
getName: () => 'Alice',
getEmail: () => 'a@b.c'
};
console.log(g.getName());
EOF
npx tsc --noEmit remap.ts
# (no errors)
# ============================================
# PART 4: KEY FILTERING
# ============================================
cat > filter.ts << 'EOF'
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type OnlyNumbers = PickByType<Mixed, number>;
// { id: number; count: number }
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
type NoNumbers = OmitByType<Mixed, number>;
// { name: string; active: boolean }
const a: OnlyNumbers = { id: 1, count: 2 };
const b: NoNumbers = { name: 'x', active: true };
console.log(a, b);
EOF
npx tsc --noEmit filter.ts
# (no errors)
# ============================================
# PART 5: MAPPING OVER UNIONS
# ============================================
cat > union.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';
type Messages = { [K in Status]: string };
const messages: Messages = {
idle: 'Waiting',
loading: 'Loading...',
ready: 'Ready',
error: 'Failed'
};
console.log(messages.idle);
EOF
npx tsc --noEmit union.ts
# (no errors)
# ============================================
# PART 6: DEEP READONLY
# ============================================
cat > deep.ts << 'EOF'
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Nested {
user: { name: string; address: { city: string } };
count: number;
}
const n: DeepReadonly<Nested> = {
user: { name: 'Alice', address: { city: 'Lisbon' } },
count: 5
};
// n.count = 10; // ❌
// n.user.name = 'Bob'; // ❌
// n.user.address.city = 'Porto'; // ❌
console.log(n);
EOF
npx tsc --noEmit deep.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts utils.ts remap.ts filter.ts union.ts deep.ts
node basic.js
# [ { id: 1, name: 'Alice' } ]
node utils.js
# [ { name: 'Alice' } { id: 1, name: 'Alice', email: 'a@b.c' } ]
node remap.js
# [ Alice ]
node filter.js
# [ { id: 1, count: 2 } { name: 'x', active: true } ]
node union.js
# [ Waiting ]
node deep.js
# [ { user: { name: 'Alice', address: { city: 'Lisbon' } }, count: 5 } ]
Quick Reference
Mapped Type Syntax
| Form | Meaning |
|---|---|
{ [K in keyof T]: T[K] } | Identity |
{ [K in keyof T]?: T[K] } | Optional |
{ readonly [K in keyof T]: T[K] } | Readonly |
{ -readonly [K in keyof T]: T[K] } | Remove readonly |
{ [K in keyof T]-?: T[K] } | Remove optional |
{ [K in U]: T } | Over union |
Modifiers
| Modifier | Effect |
|---|---|
readonly | Add readonly |
+readonly | Same as readonly |
-readonly | Remove readonly |
? | Add optional |
+? | Same as ? |
-? | Remove optional |
| (none) | Preserve from source |
Built-in Utility Types
| Type | Implementation |
|---|---|
Partial<T> | { [K in keyof T]?: T[K] } |
Required<T> | { [K in keyof T]-?: T[K] } |
Readonly<T> | { readonly [K in keyof T]: T[K] } |
Pick<T, K> | { [P in K]: T[P] } |
Record<K, V> | { [P in K]: V } |
Key Remapping
| Pattern | Result |
|---|---|
as \get${Capitalize}“ | Rename keys |
as \prefix_${K}“ | Add prefix |
as Uppercase<K> | Uppercase keys |
as T[K] extends U ? K : never | Filter keys |
Modifier Reference
| Source | Without modifier | With readonly | With -readonly |
|---|---|---|---|
readonly x | readonly x | readonly x | x |
x | x | readonly x | x |
x? | x? | readonly x? | readonly? x |
x | x | readonly x | x |
Mapping Sources
| Source | Example |
|---|---|
keyof T | Keys of a type |
| Union | 'a' | 'b' | 'c' |
keyof typeof x | Keys of a value’s type |
| Enum | Enum members |
Filtering Patterns
| Pattern | Effect |
|---|---|
PickByType<T, U> | Only keys with value type U |
OmitByType<T, U> | Keys with value type ≠ U |
PickOptional<T> | Only optional keys |
PickRequired<T> | Only required keys |
Common Custom Mapped Types
| Type | Purpose |
|---|---|
DeepReadonly<T> | Recursive readonly |
DeepPartial<T> | Recursive optional |
Getters<T> | Generate getter methods |
Setters<T> | Generate setter methods |
Nullable<T> | T[K] | null |
Promisify<T> | Promise<T[K]> |
PickByType<T, U> | Filter by value type |
ReadonlyIfArray<T> | Conditional modifier |
Utility Type Patterns
| Pattern | Meaning |
|---|---|
{ [K in keyof T]: X } | Replace all values with X |
{ [K in keyof T]: T[K] | null } | Add null to all |
{ [K in keyof T as \on${K}`]: F }` | Rename all |
{ [K in keyof T]?: T[K] } | All optional |
Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Type 'K' cannot be used to index | K not a key | Use K in keyof T |
keyof T on non-object | T is primitive | Constrain to object |
K not assignable to string | Key remap needs string | Cast with string & K |
| Cannot remove required | -? used incorrectly | Check source modifiers |
never Drops Keys
type Omit<T, K> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type OnlyString = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
as producing never removes the key entirely.
Recursion in Mapped Types
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
Values can reference the mapped type for recursive transformations.
Best Practices
✅ Do This:
// Use mapped types to derive from existing types
type Partial<T> = { [K in keyof T]?: T[K] }; // ✅
// Use modifiers to control shape
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // ✅
type Required<T> = { [K in keyof T]-?: T[K] }; // ✅
// Use `as` for key renaming
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
}; // ✅
// Use `never` to filter keys
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
}; // ✅
// Map over unions for lookup tables
type Messages = { [K in Status]: string }; // ✅
// Recursive mapped types for deep transformations
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
}; // ✅
// Cast `K` to `string` in template literals
`get${Capitalize<string & K>}` // ✅
// Guard against recursion into functions
T[K] extends Function ? T[K] : DeepReadonly<T[K]> // ✅
❌ Don’t Do This:
// Don't forget the `in` keyword
type Bad<T> = { [K in keyof T]: T[K] }; // ✅ // ✅
type Bad<T> = { [K: keyof T]: T[K] }; // ❌ syntax // ❌
// Don't use mapped types for adding new keys
type Add<T> = { [K in keyof T]: T[K]; newKey: string }; // ⚠️ // ⚠️
// Use intersections: T & { newKey: string }
// Don't map over non-union types
type Bad = { [K in number]: string }; // ⚠️ works but unusual // ⚠️
// Don't forget `string & K` in template literals
type Bad<T> = { [K in keyof T as `get${Capitalize<K>}`]: T[K] };
// ⚠️ K may not be string // ⚠️
// Don't use `as` to remove keys without `never`
type Bad<T> = { [K in keyof T as `x${K}`]: T[K] }; // ⚠️ renames // ⚠️
// Don't forget modifiers preserve by default
type Bug<T> = { [K in keyof T]: T[K] }; // keeps `readonly` // ⚠️
// Don't recurse infinitely
type Loop<T> = { [K in keyof T]: Loop<T[K]> }; // ⚠️ may loop // ⚠️
// Don't ignore `Function` in deep types
type Bad<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
}; // ⚠️ recurses into Function // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing in | Syntax error | [K in keyof T] |
| Can’t add keys | Mapped only maps existing | Use intersection |
Forgetting string & K | Template literal fails | Cast K |
| Modifiers preserved | Unexpected readonly | Use -readonly |
| Infinite recursion | Deep types loop | Guard with condition |
| Recursing into functions | Complex types blow up | Check Function |
never needed for filter | Keys not removed | Use as ... ? K : never |
| Non-string keys in remap | Compile error | Cast to string |
Real-World Examples
1. Identity map
type Clone<T> = { [K in keyof T]: T[K] };
2. Partial
type MyPartial<T> = { [K in keyof T]?: T[K] };
3. Readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
4. Mutable
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
5. Required
type MyRequired<T> = { [K in keyof T]-?: T[K] };
6. Getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
7. Setters
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void;
};
8. Pick by type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
9. Omit by type
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
10. Nullable values
type Nullable<T> = { [K in keyof T]: T[K] | null };
11. Promisify
type Promisify<T> = { [K in keyof T]: Promise<T[K]> };
12. Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
13. Deep partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
14. Lookup table
type StatusMessages = { [K in Status]: string };
15. Event handlers
type Handlers<E> = {
[K in keyof E]: (...args: E[K]) => void;
};
16. Optional by type
type OptionalIfNumber<T> = {
[K in keyof T]: T[K] extends number ? T[K] | undefined : T[K];
};
17. Prefix keys
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
18. Filter optional
type OptionalKeys<T> = {
[K in keyof T as undefined extends T[K] ? K : never]: T[K];
};
19. Filter required
type RequiredKeys<T> = {
[K in keyof T as undefined extends T[K] ? never : K]: T[K];
};
20. Chained transformations
type Transform<T> = PickByType<MyPartial<MyReadonly<T>>, string>;
Visual: Basic Mapped Type
┌──────────────────────────────────────────────┐
│ type Partial<T> = { │
│ [K in keyof T]?: T[K] │
│ }; │
│ │
│ Input: { id: number; name: string } │
│ │
└──────────────────────────────────────────────┘
│
│ for each key
▼
┌──────────────────────────────────────────────┐
│ id → id?: number │
│ name → name?: string │
│ │
│ Output: { id?: number; name?: string } │
│ │
└──────────────────────────────────────────────┘
Visual: Modifiers
┌──────────────────────────────────────────────┐
│ Source: { readonly id: number; name?: string }│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ No modifier — preserves: │
│ { [K in keyof T]: T[K] } │
│ → { readonly id: number; name?: string } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Add readonly: │
│ { readonly [K in keyof T]: T[K] } │
│ → { readonly id: number; readonly name?: string }│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Remove readonly: │
│ { -readonly [K in keyof T]: T[K] } │
│ → { id: number; name?: string } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Remove optional: │
│ { [K in keyof T]-?: T[K] } │
│ → { readonly id: number; name: string } │
│ │
└──────────────────────────────────────────────┘
Visual: Key Remapping
┌──────────────────────────────────────────────┐
│ type Getters<T> = { │
│ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]│
│ }; │
│ │
│ Input: { name: string; email: string } │
│ │
└──────────────────────────────────────────────┘
│
│ rename keys
▼
┌──────────────────────────────────────────────┐
│ name → getName │
│ email → getEmail │
│ │
│ Output: { │
│ getName: () => string; │
│ getEmail: () => string; │
│ } │
│ │
└──────────────────────────────────────────────┘
Visual: Filtering with never
┌──────────────────────────────────────────────┐
│ [K in keyof T as T[K] extends string ? K : never]: T[K]│
│ │
│ Input: { id: number; name: string; active: boolean }│
│ │
└──────────────────────────────────────────────┘
│
│ check each type
▼
┌──────────────────────────────────────────────┐
│ id → number → never → dropped │
│ name → string → keep │
│ active → boolean → never → dropped │
│ │
│ Output: { name: string } │
│ │
└──────────────────────────────────────────────┘
Visual: Mapping Over a Union
┌──────────────────────────────────────────────┐
│ type Status = 'idle' | 'loading' | 'ready'; │
│ │
│ type Messages = { [K in Status]: string }; │
│ │
└──────────────────────────────────────────────┘
│
│ for each member
▼
┌──────────────────────────────────────────────┐
│ 'idle' → idle: string │
│ 'loading' → loading: string │
│ 'ready' → ready: string │
│ │
│ Output: { │
│ idle: string; │
│ loading: string; │
│ ready: string; │
│ } │
│ │
│ Every status must be present │
│ │
└──────────────────────────────────────────────┘
Visual: Deep Readonly
┌──────────────────────────────────────────────┐
│ type DeepReadonly<T> = { │
│ readonly [K in keyof T]: T[K] extends object│
│ ? DeepReadonly<T[K]> │
│ : T[K]; │
│ }; │
│ │
│ Input: │
│ { │
│ user: { name: string; address: { city: string } };│
│ count: number; │
│ } │
│ │
└──────────────────────────────────────────────┘
│
│ recurse objects
▼
┌──────────────────────────────────────────────┐
│ count → readonly count: number │
│ user → readonly user: DeepReadonly<...> │
│ ├── name: string │
│ └── address: DeepReadonly<...> │
│ └── city: string │
│ │
│ Every level is readonly │
│ │
└──────────────────────────────────────────────┘
Visual: Utility Types Are Mapped Types
┌──────────────────────────────────────────────┐
│ Pick<T, K> │
│ = { [P in K]: T[P] } │
│ │
│ Partial<T> │
│ = { [P in keyof T]?: T[P] } │
│ │
│ Required<T> │
│ = { [P in keyof T]-?: T[P] } │
│ │
│ Readonly<T> │
│ = { readonly [P in keyof T]: T[P] } │
│ │
│ Record<K, V> │
│ = { [P in K]: V } │
│ │
│ All the same mechanism │
│ │
└──────────────────────────────────────────────┘
Visual: Combining Conditional + Mapped
┌──────────────────────────────────────────────┐
│ type PickByType<T, U> = { │
│ [K in keyof T as T[K] extends U ? K : never]: T[K]│
│ }; │
│ │
│ Iterates keys │
│ Tests each value type │
│ Filters via never │
│ Produces new type │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Need to change all properties? │
│ │ │
│ └── Yes ──► Mapped type │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need to add/remove readonly or ? │
│ │ │
│ └── Yes ──► Mapped with modifiers │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need to rename keys? │
│ │ │
│ └── Yes ──► Mapped with `as` │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need to filter by value type? │
│ │ │
│ └── Yes ──► Mapped + conditional + `as never`│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need to add new keys? │
│ │ │
│ └── Yes ──► Intersection (not mapped) │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Mapped type | { [K in keyof T]: ... } |
| Modifiers | readonly, ?, -readonly, -? |
| Key remapping | as clause |
| Key filtering | Map to never |
| Over union | K in 'a' | 'b' |
| Recursion | Reference self in values |
| Preserve modifiers | Default behavior |
| Remove modifiers | - prefix |
Key takeaways:
- A mapped type iterates the keys of a type and produces a new type
- The syntax is
{ [K in keyof T]: NewType } - Modifiers —
readonly,?— add constraints;-readonly,-?remove them - Without a modifier, the source’s modifiers are preserved
- Key remapping with
asrenames keys —as \get${Capitalize}“ - Filtering with
never—as T[K] extends U ? K : neverdrops keys - Mapped types over unions —
[K in 'a' \| 'b']— produce lookup tables - Recursive mapped types —
DeepReadonly<T>— transform nested structures - All standard utility types —
Partial,Required,Readonly,Pick,Record— are mapped types - Combine with conditional types for value-based transformations
string & Kin template literals ensuresKis a string- Guard against recursion into functions in deep transformations
Remember: Mapped types are how you transform one object type into another. Iterate the keys, apply a rule, and produce the new type. Modifiers control readonly and optionality. as renames and filters keys. Conditional types decide per-property transformations. Every standard utility type is built on this mechanism — once you understand it, you can write your own utilities that do exactly what you need. It’s the bridge between simple types and the full power of TypeScript’s type system.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!