| |

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 key K of T
  • ?: — make the property optional
  • T[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 as with never)
  • 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.map maps over elements. Each key becomes a property in the new type, with a rule applied. The syntax [K in ...] is the type-level equivalent of for — 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. The in keyword 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:

ModifierEffect
(none)Preserves existing modifier
readonlyAdds readonly
+readonlySame as readonly
-readonlyRemoves 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 — -readonly means “remove readonly,” -? means “remove optional.” It’s a small syntax that unlocks Mutable and Required as the inverses of Readonly and Partial.


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 keys
  • as \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 never removes keys: In TypeScript, a property with type never is meaningless — no value can inhabit it. When as produces never, the compiler drops the property entirely. That’s the mechanism for key filtering: map unwanted keys to never.


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 U is more general than K in keyof T: keyof T is a union of keys. K in U for any union U is the same mechanism. keyof T is just a special case where U happens 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 optional
  • Readonly<AppState> — every property readonly
  • DeepReadonly<T> — recursive readonly
  • Pick and Omit — subsets of properties
  • Selectors — mapped type renaming keys to select* methods
  • Setters — mapped type renaming keys to set* 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

FormMeaning
{ [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

ModifierEffect
readonlyAdd readonly
+readonlySame as readonly
-readonlyRemove readonly
?Add optional
+?Same as ?
-?Remove optional
(none)Preserve from source

Built-in Utility Types

TypeImplementation
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

PatternResult
as \get${Capitalize}“Rename keys
as \prefix_${K}“Add prefix
as Uppercase<K>Uppercase keys
as T[K] extends U ? K : neverFilter keys

Modifier Reference

SourceWithout modifierWith readonlyWith -readonly
readonly xreadonly xreadonly xx
xxreadonly xx
x?x?readonly x?readonly? x
xxreadonly xx

Mapping Sources

SourceExample
keyof TKeys of a type
Union'a' | 'b' | 'c'
keyof typeof xKeys of a value’s type
EnumEnum members

Filtering Patterns

PatternEffect
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

TypePurpose
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

PatternMeaning
{ [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

ErrorCauseFix
Type 'K' cannot be used to indexK not a keyUse K in keyof T
keyof T on non-objectT is primitiveConstrain to object
K not assignable to stringKey remap needs stringCast with string & K
Cannot remove required-? used incorrectlyCheck 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

PitfallProblemSolution
Missing inSyntax error[K in keyof T]
Can’t add keysMapped only maps existingUse intersection
Forgetting string & KTemplate literal failsCast K
Modifiers preservedUnexpected readonlyUse -readonly
Infinite recursionDeep types loopGuard with condition
Recursing into functionsComplex types blow upCheck Function
never needed for filterKeys not removedUse as ... ? K : never
Non-string keys in remapCompile errorCast 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

ConceptMeaning
Mapped type{ [K in keyof T]: ... }
Modifiersreadonly, ?, -readonly, -?
Key remappingas clause
Key filteringMap to never
Over unionK in 'a' | 'b'
RecursionReference self in values
Preserve modifiersDefault 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 }
  • Modifiersreadonly, ? — add constraints; -readonly, -? remove them
  • Without a modifier, the source’s modifiers are preserved
  • Key remapping with as renames keys — as \get${Capitalize}“
  • Filtering with neveras T[K] extends U ? K : never drops keys
  • Mapped types over unions[K in 'a' \| 'b'] — produce lookup tables
  • Recursive mapped typesDeepReadonly<T> — transform nested structures
  • All standard utility typesPartial, Required, Readonly, Pick, Record — are mapped types
  • Combine with conditional types for value-based transformations
  • string & K in template literals ensures K is 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!