TypeScript Mapped Types Deep Dive: Transform Types Like a Pro
Master TypeScript's mapped types to build custom type transformations. Learn keyof iteration, property modifiers, and key remapping to create bulletproof utility types.
While building a form library a few months ago, I hit a wall. I needed to create a type that turned every property into a validation function. After hours of Stack Overflow and documentation diving, I discovered mapped types—and it completely changed how I think about TypeScript.
Here's the thing: if you've ever used Partial<T>, Required<T>, or Readonly<T>, you've already benefited from mapped types. But understanding how they work under the hood? That's what separates developers who use TypeScript from developers who master it. If you're not familiar with generics yet, I recommend checking out my generics guide first—mapped types build directly on that foundation.
In this post, we'll go from the fundamentals all the way to advanced patterns you can use in production. Let's dive in.
1. The Basics: Iterating Over Keys
The core syntax of mapped types looks like this:
type MappedType<T> = {
[K in keyof T]: SomeTransformation
}Let me break this down:
keyof Tproduces a union of all property names inTK in keyof Titerates over each key- The right side defines what type each property should have
Here's a simple example that makes all properties booleans:
type MakeBoolean<T> = {
[K in keyof T]: boolean
}
interface User {
name: string
age: number
isAdmin: boolean
}
type UserFlags = MakeBoolean<User>
// Result: { name: boolean; age: boolean; isAdmin: boolean }Now here's where it gets interesting. Remember how I said Partial<T> uses mapped types? Here's literally how it's implemented:
type Partial<T> = {
[K in keyof T]?: T[K]
}That ? after the brackets makes each property optional. The T[K] syntax accesses the original type of each property, so we're preserving the original types while making them optional.
Key takeaway: The [K in keyof T] pattern lets you iterate over every property in a type and transform it however you need.
2. Property Modifiers: The Plus and Minus Magic
TypeScript gives you two modifiers you can add or remove from properties: readonly and ? (optional). The secret sauce is the + and - prefixes.
Want to make all properties optional? Add ?:
type MakeOptional<T> = {
[K in keyof T]?: T[K]
}Want to make all properties required? Remove ? with -?:
type MakeRequired<T> = {
[K in keyof T]-?: T[K]
}Want to make everything readonly? Add readonly:
type MakeReadonly<T> = {
readonly [K in keyof T]: T[K]
}And here's the one I use constantly when I need mutable objects from immutable ones—remove readonly:
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
interface FrozenConfig {
readonly apiUrl: string
readonly timeout: number
}
type EditableConfig = Mutable<FrozenConfig>
// Result: { apiUrl: string; timeout: number } - no longer readonly!
This is exactly how the built-in Required<T> and Readonly<T> utilities work. Once you understand this pattern, you can create your own custom modifiers for any situation.
Key takeaway: Use + and - before readonly or ? to add or remove these modifiers from all properties at once.
3. Key Remapping with the as Clause
This is where mapped types go from "useful" to "mind-blowing." The as clause lets you rename keys during iteration. Combined with template literal types, you can generate entirely new property names.
Want to create getter methods for every property?
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
}
type PersonGetters = Getters<Person>
// Result: { getName: () => string; getAge: () => number }The string & K part is necessary because keyof T can include symbols, and we need to ensure we're only working with string keys for template literals.
Here's another pattern I use for event handlers:
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void
}
interface FormData {
username: string
email: string
age: number
}
type FormHandlers = EventHandlers<FormData>
// Result: {
// onUsernameChange: (value: string) => void
// onEmailChange: (value: string) => void
// onAgeChange: (value: number) => void
// }You can also filter out certain keys using as with never:
type RemoveKind<T> = {
[K in keyof T as Exclude<K, 'kind'>]: T[K]
}
interface Tagged {
kind: string
id: number
name: string
}
type WithoutKind = RemoveKind<Tagged>
// Result: { id: number; name: string }Key takeaway: The as clause transforms key names. Use it with template literals to generate new property names, or with never to filter keys out.
4. Combining with Conditional Types
When you combine mapped types with conditional types, you unlock even more powerful patterns. Want to extract only the function properties from an object type? If you need a refresher on conditional types, check out my conditional types guide.
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T]
interface Service {
id: number
name: string
start: () => void
stop: () => void
}
type ServiceMethods = FunctionKeys<Service>
// Result: "start" | "stop"Let me walk through what's happening here:
- We iterate over each key and check if its value extends a function type
- If it does, we keep the key; if not, we return
never - The
[keyof T]at the end extracts all values, andnevertypes get filtered out of unions
Here's the inverse—getting non-function keys:
type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K
}[keyof T]
type ServiceData = NonFunctionKeys<Service>
// Result: "id" | "name"Now you can use these to pick specific properties:
type PickFunctions<T> = Pick<T, FunctionKeys<T>>
type PickData<T> = Pick<T, NonFunctionKeys<T>>
type ServiceMethodsOnly = PickFunctions<Service>
// Result: { start: () => void; stop: () => void }
type ServiceDataOnly = PickData<Service>
// Result: { id: number; name: string }Key takeaway: Combine mapped types with conditional types to create dynamic property filtering based on value types, not just key names.

5. Real-World Patterns
Let me show you three patterns I use regularly in production code.
Pattern 1: Form Validation Types
Remember my form library problem from the beginning? Here's how I solved it:
type Validators<T> = {
[K in keyof T]: (value: T[K]) => boolean
}
interface RegistrationForm {
email: string
password: string
age: number
}
type RegistrationValidators = Validators<RegistrationForm>
// Result: {
// email: (value: string) => boolean
// password: (value: string) => boolean
// age: (value: number) => boolean
// }
const validators: RegistrationValidators = {
email: (value) => value.includes('@'),
password: (value) => value.length >= 8,
age: (value) => value >= 18
}Each validator function receives the correct type for its corresponding field. If someone tries to pass a string validator for the age field, TypeScript catches it immediately.
Pattern 2: Event Handler Types
This pattern generates type-safe event handlers for any object shape:
type ChangeHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
newValue: T[K],
oldValue: T[K]
) => void
}
interface Settings {
theme: 'light' | 'dark'
fontSize: number
notifications: boolean
}
type SettingsHandlers = ChangeHandlers<Settings>
// Result: {
// onThemeChange: (newValue: 'light' | 'dark', oldValue: 'light' | 'dark') => void
// onFontSizeChange: (newValue: number, oldValue: number) => void
// onNotificationsChange: (newValue: boolean, oldValue: boolean) => void
// }Pattern 3: Deep Readonly
Sometimes you need to make an entire object tree immutable:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends (...args: any[]) => any
? T[K]
: DeepReadonly<T[K]>
: T[K]
}
interface Config {
server: {
host: string
port: number
ssl: {
enabled: boolean
cert: string
}
}
logging: {
level: string
}
}
type ImmutableConfig = DeepReadonly<Config>
// All nested properties are now readonly!The conditional check for functions prevents us from trying to recurse into function types, which would cause issues.
Key takeaway: These patterns aren't just theoretical—they solve real problems in production codebases. Start with simple mapped types and gradually combine them with other TypeScript features.
Conclusion
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!
Mapped types might seem complex at first, but once you understand the core pattern of [K in keyof T], everything else builds on it. Property modifiers give you control over readonly and optional markers. Key remapping with as lets you transform property names. And combining these with conditional types opens up nearly unlimited possibilities.
The point is: understanding mapped types transforms how you think about TypeScript. Instead of writing repetitive type definitions, you start thinking in terms of type transformations. Your types become more maintainable, more DRY, and more powerful.
If you're looking to go deeper, check out my guide on 10 TypeScript utility types for bulletproof code—now that you understand how mapped types work, you'll see exactly how those utilities are implemented.
What type transformation are you going to build first? Let me know on Twitter/X!
Related Posts
If you enjoyed this guide, you might also like:
- Generics in TypeScript - The foundation that makes mapped types possible
- 5 TypeScript Conditional Types Patterns - Combine with mapped types for ultimate power
- 10 TypeScript Utility Types for Bulletproof Code - See mapped types in action
Photo credits: Mikhail Nilov and cottonbro studio on Pexels