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 beundefined)
Both can appear in any object type literal. Mapped types let you add or remove them programmatically across all keys at once.
// 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 propertyAdding 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).
// 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 }+ 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.
// 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 }-? 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:
// 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 keptKey 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.
// 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.
// 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 }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<S>—"hello"→"HELLO"Lowercase<S>—"HELLO"→"hello"Capitalize<S>—"hello"→"Hello"Uncapitalize<S>—"Hello"→"hello"
// 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.
// 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.
// 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) |
+?, -?, +readonly, -readonly) and key remapping with as. These tools let you transform any object type with surgical precision.