TypeScript Mapped Types — Transform Types
Advertisement
Mapped types let you generate new types by iterating over the properties of existing types. They're the foundation of powerful abstractions and eliminating type duplication.
Basic Mapped Types
A mapped type transforms each property of a source type:
interface User {
id: number;
name: string;
email: string;
}
// Make all properties readonly
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// Make all properties optional
type PartialUser = {
[K in keyof User]?: User[K];
};
// Make all properties nullable
type NullableUser = {
[K in keyof User]: User[K] | null;
};
// Make all properties getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
Remapping Keys with as
interface APIResponse {
user_id: number;
user_name: string;
user_email: string;
}
// Convert snake_case to camelCase
type Camelcase<T> = {
[K in keyof T as K extends `${infer Prefix}_${infer Rest}`
? `${Prefix}${Capitalize<Rest>}`
: K]: T[K];
};
type CamelcaseResponse = Camelcase<APIResponse>;
// {
// userId: number;
// userName: string;
// userEmail: string;
// }
// Filter properties by type
type StringPropertiesOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringPropertiesOnly<User>;
// {
// name: string;
// email: string;
// }
- Basic Mapped Types
- Remapping Keys with as
- Conditional Types with Mapped Types
- Common Mapped Type Patterns
- Getters and Setters
- Record Types
- Proxy Types
- Form State
- Advanced Patterns
- Recursive Mapped Types
- Template Literal Types with Mapped Types
- Practical Example: API Client
- Performance Considerations
- FAQ
Conditional Types with Mapped Types
// Get only readable properties
type Readable<T> = {
[K in keyof T]-?: T[K] extends Function ? never : K;
}[keyof T];
interface Mixed {
name: string;
getName(): string;
age: number;
}
type ReadableProps = Readable<Mixed>; // "name" | "age"
// Create getters only for non-function properties
type Accessors<T> = {
[K in keyof T as T[K] extends Function
? never
: `get${Capitalize<string & K>}`]: T[K] extends Function
? never
: () => T[K];
};
type UserAccessors = Accessors<User>;
Common Mapped Type Patterns
Getters and Setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (
value: T[K]
) => void;
};
type GettersAndSetters<T> = Getters<T> & Setters<T>;
class UserClass {
private data: User = { id: 1, name: "", email: "" };
getId(): number {
return this.data.id;
}
setId(value: number): void {
this.data.id = value;
}
getName(): string {
return this.data.name;
}
setName(value: string): void {
this.data.name = value;
}
getEmail(): string {
return this.data.email;
}
setEmail(value: string): void {
this.data.email = value;
}
}
type UserClassInterface = GettersAndSetters<User>;
Record Types
// Create a mapping from string to another type
type Permission = "read" | "write" | "delete";
type PermissionFlags = Record<Permission, boolean>;
// {
// read: boolean;
// write: boolean;
// delete: boolean;
// }
// Create a type from a union
type EventMap = {
"user:created": { userId: number };
"user:deleted": { userId: number };
"post:published": { postId: number };
};
type EventListeners<T extends keyof EventMap = keyof EventMap> = {
[K in T]: (payload: EventMap[K]) => void;
};
const listeners: EventListeners = {
"user:created": (payload) => console.log(payload.userId),
"user:deleted": (payload) => console.log(payload.userId),
"post:published": (payload) => console.log(payload.postId),
};
Proxy Types
// Create a proxy handler type
type Proxify<T> = {
get(target: T, prop: keyof T): T[keyof T];
set(target: T, prop: keyof T, value: T[keyof T]): boolean;
};
type ProxyObject<T> = {
[K in keyof T]: T[K];
};
// Extract property names by type
type MethodNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface DataService {
getName(): string;
getAge(): number;
processData(data: any): void;
}
type Methods = MethodNames<DataService>; // "getName" | "getAge" | "processData"
Form State
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
// Create error state
type FormErrors<T> = Partial<Record<keyof T, string>>;
// Create touched state
type FormTouched<T> = Partial<Record<keyof T, boolean>>;
// Create loaded state
type FormLoaded<T> = {
[K in keyof T]: boolean;
};
// Complete form state
type FormState<T> = {
values: T;
errors: FormErrors<T>;
touched: FormTouched<T>;
loaded: FormLoaded<T>;
isSubmitting: boolean;
isValid: boolean;
};
type LoginFormState = FormState<LoginForm>;
const formState: LoginFormState = {
values: { email: "", password: "", rememberMe: false },
errors: { email: "Required" },
touched: { email: true },
loaded: { email: true, password: false, rememberMe: false },
isSubmitting: false,
isValid: false,
};
Advanced Patterns
Recursive Mapped Types
// Deep partial
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// Can omit any nested property
// Deep readonly
type DeepReadonly<T> = T extends object
? {
readonly [K in keyof T]: DeepReadonly<T[K]>;
}
: T;
type ImmutableConfig = DeepReadonly<Config>;
// All nested objects and properties are readonly
Template Literal Types with Mapped Types
// Create event listeners from event names
type Events = "click" | "hover" | "focus";
type EventHandlers = {
[K in Events as `on${Capitalize<K>}`]: (event: Event) => void;
};
// {
// onClick: (event: Event) => void;
// onHover: (event: Event) => void;
// onFocus: (event: Event) => void;
// }
// Create routes from actions
type Action = "list" | "create" | "update" | "delete";
type Routes = {
[K in Action as `/${K}`]: (req: any, res: any) => void;
};
// {
// /list: (req: any, res: any) => void;
// /create: (req: any, res: any) => void;
// /update: (req: any, res: any) => void;
// /delete: (req: any, res: any) => void;
// }
Practical Example: API Client
interface UserAPI {
getUser(id: number): Promise<User>;
createUser(data: Omit<User, "id">): Promise<User>;
updateUser(id: number, data: Partial<User>): Promise<User>;
deleteUser(id: number): Promise<void>;
}
// Create mock methods that return fixed values
type MockAPI<T extends Record<string, (...args: any[]) => any>> = {
[K in keyof T]: (...args: Parameters<T[K]>) => ReturnType<T[K]>;
};
const mockUserAPI: MockAPI<UserAPI> = {
getUser: async (id: number) => ({
id,
name: "Mock User",
email: "mock@example.com",
}),
createUser: async (data) => ({
id: 1,
...data,
}),
updateUser: async (id: number, data: any) => ({
id,
name: "Mock User",
email: "mock@example.com",
...data,
}),
deleteUser: async () => {},
};
Performance Considerations
- Avoid deeply nested mapped types - they slow compilation
- Use
keyof Tcarefully - it can create massive types - Cache computed types - TypeScript caches them automatically
FAQ
Q: When should I use mapped types vs generics? A: Use mapped types when you need to transform all properties of a type. Use generics for flexible constraints. Often you combine both.
Q: Can mapped types filter out properties? A: Yes, using as with never. Properties that map to never are removed.
Q: Do mapped types have performance impact? A: Only at compile time. Zero runtime impact. Very complex mapped types can slow compilation, so keep them reasonable.
Mapped types are TypeScript's most powerful abstraction tool. They eliminate duplication, enforce consistency, and enable patterns impossible with plain interfaces. Master them and you'll write less code that's more maintainable.
Advertisement