Mapped Types in TypeScript
Welcome! Mapped types allow you to create new types by transforming properties of existing types. They’re incredibly powerful! Let’s dive in! 🚀
What Are Mapped Types?
Mapped types transform each property of an existing type into a new property. Think of them as a map() function for types.
Basic Syntax
type MappedType<T> = { [K in keyof T]: NewType;};Meaning: For each key K in type T, create a new property with type NewType.
Basic Mapped Types
Making All Properties Optional
type MyPartial<T> = { [K in keyof T]?: T[K];};
interface User { id: string; name: string; email: string;}
type PartialUser = MyPartial<User>;// Result: { id?: string; name?: string; email?: string; }Making All Properties Required
type MyRequired<T> = { [K in keyof T]-?: T[K];};
interface Config { host?: string; port?: number;}
type RequiredConfig = MyRequired<Config>;// Result: { host: string; port: number; }Note: The -? removes the optional modifier.
Making All Properties Readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K];};
interface Settings { theme: string; language: string;}
type ReadonlySettings = MyReadonly<Settings>;// Result: { readonly theme: string; readonly language: string; }Removing Modifiers
Removing readonly
type Mutable<T> = { -readonly [K in keyof T]: T[K];};
interface LockedUser { readonly id: string; readonly name: string;}
type MutableUser = Mutable<LockedUser>;// Result: { id: string; name: string; }Modifier Combinations
type MutableRequired<T> = { -readonly [K in keyof T]-?: T[K];};
interface MaybeReadonly { readonly id?: string; readonly name?: string;}
type FullyMutable = MutableRequired<MaybeReadonly>;// Result: { id: string; name: string; }Key Remapping with as
The as clause allows you to rename or filter keys while mapping.
Renaming Keys
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};
interface User { name: string; age: number;}
type UserGetters = Getters<User>;// Result: { getName: () => string; getAge: () => number; }Filtering Keys
type RemoveId<T> = { [K in keyof T as K extends "id" ? never : K]: T[K];};
interface Product { id: string; name: string; price: number;}
type ProductWithoutId = RemoveId<Product>;// Result: { name: string; price: number; }Filtering by Property Type
type StringPropertiesOnly<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K];};
interface Person { name: string; age: number; email: string; isActive: boolean;}
type PersonStrings = StringPropertiesOnly<Person>;// Result: { name: string; email: string; }Template Literal Types in Mapped Types
Combine template literals with mapped types for powerful transformations.
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;// }Pluralization
type Pluralize<T> = { [K in keyof T as `${string & K}s`]: T[K][];};
interface Item { product: string; category: string;}
type Items = Pluralize<Item>;// Result: { products: string[]; categories: string[]; }Conditional Types with Mapped Types
Combine mapped types with conditional logic:
type Nullable<T> = { [K in keyof T]: T[K] extends object ? Nullable<T[K]> : T[K] | null;};
interface Config { server: { host: string; port: number; }; timeout: number;}
type NullableConfig = Nullable<Config>;// Result: {// server: { host: string | null; port: number | null; };// timeout: number | null;// }How Utility Types Work Internally
Understanding mapped types helps you see how built-in utility types work.
Partial<T>
type Partial<T> = { [K in keyof T]?: T[K];};Required<T>
type Required<T> = { [K in keyof T]-?: T[K];};Readonly<T>
type Readonly<T> = { readonly [K in keyof T]: T[K];};Pick<T, K>
type Pick<T, K extends keyof T> = { [P in K]: T[P];};Omit<T, K>
type Omit<T, K extends keyof any> = { [P in keyof T as P extends K ? never : P]: T[P];};Record<K, T>
type Record<K extends keyof any, T> = { [P in K]: T;};Advanced Patterns
Deep Partial
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];};
interface NestedConfig { database: { host: string; port: number; credentials: { username: string; password: string; }; };}
type PartialNestedConfig = DeepPartial<NestedConfig>;// All nested properties are optionalType-safe Property Paths
type PropertyPaths<T, Prefix extends string = ""> = { [K in keyof T]: T[K] extends object ? PropertyPaths<T[K], `${Prefix}${string & K}.`> | `${Prefix}${string & K}` : `${Prefix}${string & K}`;}[keyof T];
interface User { profile: { name: string; age: number; }; settings: { theme: string; };}
type UserPaths = PropertyPaths<User>;// Result: "profile" | "profile.name" | "profile.age" | "settings" | "settings.theme"Practical Examples
Form State Management
type FormState<T> = { [K in keyof T]: { value: T[K]; error: string | null; touched: boolean; };};
interface LoginForm { email: string; password: string;}
type LoginFormState = FormState<LoginForm>;// Result: {// email: { value: string; error: string | null; touched: boolean; };// password: { value: string; error: string | null; touched: boolean; };// }API Response Wrappers
type AsyncData<T> = { [K in keyof T]: { data: T[K] | null; loading: boolean; error: Error | null; };};
interface UserData { profile: { name: string; email: string }; posts: Array<{ title: string; content: string }>;}
type AsyncUserData = AsyncData<UserData>;// Each property is wrapped with loading stateValidation Schema
type ValidationRules<T> = { [K in keyof T]?: { required?: boolean; minLength?: number; maxLength?: number; pattern?: RegExp; };};
interface SignupForm { username: string; email: string; password: string;}
const validationRules: ValidationRules<SignupForm> = { username: { required: true, minLength: 3, maxLength: 20 }, email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }, password: { required: true, minLength: 8 }};Key Takeaways
- Mapped types transform properties of existing types
- Use
[K in keyof T]to iterate over type keys - Add/remove modifiers with
?,readonly,-?,-readonly - Key remapping with
asallows filtering and renaming - Template literal types enable dynamic key transformations
- Mapped types power many built-in utility types
- Combine with conditional types for complex transformations
💡 Conclusion
Mapped types are one of TypeScript’s most powerful features for type transformations. Understanding them deeply will help you create sophisticated, reusable type utilities and better understand how TypeScript’s built-in types work!
Happy coding with TypeScript! 🎉