TypeScriptMapped Type Modifiers & Key Remapping

Mapped Type Modifiers & Key Remapping

Mapped types have two advanced features that give you fine-grained control over the output type: modifiers (for adding or removing readonly and ?) and key remapping (for renaming or filtering keys using as). Together they cover almost every object type transformation you'll ever need.

Quick Recap — Modifiers in Regular Types

Before diving into mapped modifiers, recall how modifiers work in ordinary types:

  • readonly — property cannot be reassigned after initialisation
  • ? — property is optional (the value may be undefined)

Both can appear in any object type literal. Mapped types let you add or remove them programmatically across all keys at once.

TS
// Normal usage of modifiers
interface User {
  readonly id: number;  // immutable
  name: string;
  email?: string;       // optional
}

const u: User = { id: 1, name: "Alice" };
// u.id = 2; // Error: cannot assign to 'id' because it is a read-only property
Adding Modifiers with + (or no prefix)

Inside a mapped type you can add readonly or ? with a + prefix (the + is optional — omitting it means the same thing).

TS
// These two are identical — + is the default
type MyPartial<T>  = { [K in keyof T]+?: T[K] };
type MyPartial2<T> = { [K in keyof T]?:  T[K] };

type MyReadonly<T>  = { +readonly [K in keyof T]: T[K] };
type MyReadonly2<T> = {  readonly [K in keyof T]: T[K] };

interface Config {
  host: string;
  port: number;
}

type PartialConfig   = MyPartial<Config>;   // { host?: string; port?: number }
type ReadonlyConfig  = MyReadonly<Config>;  // { readonly host: string; readonly port: number }
Note
The explicit + prefix is rarely used in practice but helps document intent when mixing + and - modifiers in the same mapped type.
Removing Modifiers with -

The - prefix removes a modifier that may exist on the source type. This is how TypeScript's built-in Required and Mutable (non-built-in, but common) utilities work.

TS
// Remove optional modifier — equivalent to built-in Required<T>
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Remove readonly modifier (not built-in, but often needed)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove both at once
type WritableRequired<T> = {
  -readonly [K in keyof T]-?: T[K];
};

interface PartialUser {
  readonly id?: number;
  readonly name?: string;
}

type FullUser    = MyRequired<PartialUser>;      // { readonly id: number; readonly name: string }
type MutableUser = Mutable<PartialUser>;         // { id?: number; name?: string }
type CleanUser   = WritableRequired<PartialUser>; // { id: number; name: string }
Tip
The -? modifier is the idiomatic way to implement Required<T>. Without it you would need a conditional type workaround.
Mixing + and - Modifiers

You can combine readonly and ? modifiers independently — add one while removing the other:

TS
// Add readonly, remove optional
type FrozenRequired<T> = {
  +readonly [K in keyof T]-?: T[K];
};

// Remove readonly, add optional
type MutablePartial<T> = {
  -readonly [K in keyof T]+?: T[K];
};

interface Snapshot {
  readonly timestamp?: number;
  readonly data?: string;
}

type Live = MutablePartial<Snapshot>;
// { timestamp?: number; data?: string }
// — readonly removed, ? kept (already optional, but + is explicit)

type Archived = FrozenRequired<Snapshot>;
// { readonly timestamp: number; readonly data: string }
// — ? removed, readonly kept
Key Remapping with as (TypeScript 4.1+)

TypeScript 4.1 introduced the as clause inside mapped types, allowing you to rename each key as you iterate. The syntax is:

{ [K in keyof T as NewKeyExpression]: T[K] }

NewKeyExpression is a type that resolves to a string literal, number literal, or symbol — essentially any valid key type.

TS
// Prefix every key with "get" (using template literal types)
type Getters<T> = {
  [K in keyof T as `get_${string & K}`]: () => T[K];
};

interface Point {
  x: number;
  y: number;
}

type PointGetters = Getters<Point>;
// { get_x: () => number; get_y: () => number }

// Suffix every key with "_raw"
type RawKeys<T> = {
  [K in keyof T as `${string & K}_raw`]: T[K];
};

type RawPoint = RawKeys<Point>;
// { x_raw: number; y_raw: number }
Filtering Keys with as never

When the as expression resolves to never, TypeScript drops that key from the output type entirely. This is the idiomatic way to filter keys in a mapped type without an external Pick or Omit.

TS
// Keep only keys whose value type extends the given type
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  score: number;
  label: string;
}

type StringProps  = PickByValue<Mixed, string>;   // { name: string; label: string }
type NumberProps  = PickByValue<Mixed, number>;   // { id: number; score: number }

// Remove keys whose value type extends the given type
type OmitByValue<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K];
};

type NoStrings = OmitByValue<Mixed, string>;
// { id: number; active: boolean; score: number }
Note
This is more powerful than built-in Pick/Omit because it filters by value type rather than by key name.
Renaming Keys with Capitalize and Friends

TypeScript ships four intrinsic string manipulation types that work well inside as clauses:

  • Uppercase&lt;S&gt;"hello""HELLO"
  • Lowercase&lt;S&gt;"HELLO""hello"
  • Capitalize&lt;S&gt;"hello""Hello"
  • Uncapitalize&lt;S&gt;"Hello""hello"

TS
// Convert all keys to uppercase
type UpperKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

interface Config {
  host: string;
  port: number;
}

type UpperConfig = UpperKeys<Config>;
// { HOST: string; PORT: number }

// Generate setter method names: name → setName
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type UserSetters = Setters<{ id: number; name: string }>;
// { setId: (value: number) => void; setName: (value: string) => void }
Combining Remapping with Conditional Types

The as clause can contain any type expression — including conditional types. This lets you make key-level decisions based on the value type at each key.

TS
// Rename string-valued keys to "str_<key>", leave others unchanged
type PrefixStringKeys<T> = {
  [K in keyof T as T[K] extends string
    ? `str_${string & K}`
    : K
  ]: T[K];
};

interface Record {
  id: number;
  name: string;
  score: number;
  tag: string;
}

type Prefixed = PrefixStringKeys<Record>;
// {
//   id: number;
//   str_name: string;
//   score: number;
//   str_tag: string;
// }

// Filter to function-type values and rename with "call_" prefix
type CallableKeys<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any
    ? `call_${string & K}`
    : never
  ]: T[K];
};
Practical Pattern — Event Map to Handler Map

A common real-world use: given an event payload map, generate the corresponding handler (listener) map automatically.

TS
// Given: event name → payload type
interface AppEvents {
  userLogin: { userId: string; timestamp: number };
  userLogout: { userId: string };
  pageView: { path: string; referrer: string };
  error: { code: number; message: string };
}

// Generate: event name → handler function type
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void;
};

type AppHandlers = EventHandlers<AppEvents>;
// {
//   onUserLogin:  (payload: { userId: string; timestamp: number }) => void;
//   onUserLogout: (payload: { userId: string }) => void;
//   onPageView:   (payload: { path: string; referrer: string }) => void;
//   onError:      (payload: { code: number; message: string }) => void;
// }

// Now you can type-check an event emitter implementation
class Emitter {
  private handlers: Partial<AppHandlers> = {};

  on<K extends keyof AppHandlers>(event: K, handler: AppHandlers[K]) {
    this.handlers[event] = handler;
  }
}
Modifier Quick Reference

Syntax

Effect

Example output

[K in keyof T]?:

Add optional

name?: string

[K in keyof T]+?:

Add optional (explicit)

name?: string

[K in keyof T]-?:

Remove optional

name: string

readonly [K in keyof T]:

Add readonly

readonly name: string

+readonly [K in keyof T]:

Add readonly (explicit)

readonly name: string

-readonly [K in keyof T]:

Remove readonly

name: string

[K in keyof T as NewK]:

Remap key to NewK

newKey: string

[K in keyof T as never]:

Drop key

(key omitted)

Success
You now understand all four mapped type modifiers (+?, -?, +readonly, -readonly) and key remapping with as. These tools let you transform any object type with surgical precision.