What Are Mapped Types?
A mapped type creates a new type by iterating over the keys of an existing type:
type Optional<T> = {
[K in keyof T]?: T[K]
}
// Equivalent to Partial<T> โ same idea as built-in utility types
interface User {
id: string
name: string
email: string
}
type OptionalUser = Optional<User>
// { id?: string; name?: string; email?: string }
The [K in keyof T] is the mapped type syntax. It says: "for each key K in T, create a property."
Modifiers: ? and readonly
You can add or remove ? (optional) and readonly modifiers:
// Add readonly
type Immutable<T> = {
readonly [K in keyof T]: T[K]
}
// Remove readonly (note the -)
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
// Remove optional
type Required<T> = {
[K in keyof T]-?: T[K]
}
The - prefix removes a modifier. This is how TypeScript's built-in Required<T> is implemented.
Transforming Value Types
Change what each property contains:
// Wrap every value in a Promise
type Async<T> = {
[K in keyof T]: Promise<T[K]>
}
// Wrap every value in a getter function
type Lazy<T> = {
[K in keyof T]: () => T[K]
}
// Make every value nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null
}
Key Remapping with as
Remap key names in the mapped type:
// Add "get" prefix to all methods
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Store {
count: number
name: string
}
type StoreGetters = Getters<Store>
// { getCount: () => number; getName: () => string }
Template Literal Types
Template literal types work like JavaScript template strings but at the type level:
type EventName = 'click' | 'focus' | 'blur'
// Generate 'onClick' | 'onFocus' | 'onBlur'
type EventHandler = `on${Capitalize<EventName>}`
Real Example: CSS Properties
type Side = 'top' | 'right' | 'bottom' | 'left'
type Axis = 'x' | 'y'
// Generate 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'
type MarginProp = `margin-${Side}`
// Generate 'padding-x' | 'padding-y'
type PaddingAxis = `padding-${Axis}`
Real Example: Typed Event Emitter
type EventMap = {
userCreated: { userId: string }
orderPlaced: { orderId: string; total: number }
emailSent: { to: string }
}
type EventHandler<T extends keyof EventMap> = (payload: EventMap[T]) => void
// Generate `onUserCreated`, `onOrderPlaced`, etc.
type EventHandlers = {
[K in keyof EventMap as `on${Capitalize<string & K>}`]: EventHandler<K>
}
// Type-safe event bus
interface EventBus extends EventHandlers {
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void
}
Filtering Keys with Conditional Types
Remap keys to never to filter them out:
// Only keep string-valued properties
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
interface Mixed {
name: string
age: number
email: string
active: boolean
}
type StringOnly = StringKeys<Mixed>
// { name: string; email: string }
Combining Mapped + Template Types: Deep Transform
// Flatten a nested object type into dot-notation keys
type DotPaths<T, Prefix extends string = ''> = {
[K in keyof T & string]: T[K] extends object
? DotPaths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
}[keyof T & string]
interface Config {
db: { host: string; port: number }
app: { name: string; debug: boolean }
}
type ConfigPaths = DotPaths<Config>
// 'db.host' | 'db.port' | 'app.name' | 'app.debug'
// Now you can type-check config lookups
function getConfig<P extends ConfigPaths>(path: P): string { ... }
getConfig('db.host') // โ
getConfig('db.wrong') // โ Error
Key Takeaways
- Mapped types let you transform every property of a type programmatically
- Use
-?and-readonlyto remove modifiers - Key remapping with
aslets you rename keys, including with template literals - Template literal types work like JS template strings at the type level
- Filter keys by mapping to
neverin the key remapping clause - These features compose โ mapped + conditional + template = very powerful transformations