The Repetition Problem
// You have this type
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// You need these too
interface UserUpdate {
id?: string; // same fields, all optional
name?: string;
email?: string;
role?: 'admin' | 'user';
}
interface UserReadonly {
readonly id: string; // same fields, all readonly
readonly name: string;
readonly email: string;
readonly role: 'admin' | 'user';
}
Three types, almost identical. Add a field to User and you have to update three places.
Mapped types solve this.
The Syntax
// Map over keys, transform the type
type Mapped<T> = {
[K in keyof T]: T[K];
};
// This is exactly equivalent to T
type UserCopy = Mapped<User>;
Now add modifiers:
// Make all fields optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Make all fields readonly
type Immutable<T> = {
readonly [K in keyof T]: T[K];
};
// Make all fields nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// These are just how Partial<T>, Readonly<T> are defined
Built-in Mapped Types
// Partial: all fields optional
type UpdateUser = Partial<User>;
// { id?: string; name?: string; email?: string; role?: ... }
// Required: all fields required
type RequiredUser = Required<Partial<User>>;
// Readonly: all fields readonly
type ImmutableUser = Readonly<User>;
// Record: create object type with specified keys and value type
type UserRecord = Record<string, User>;
type StatusMap = Record<'active' | 'inactive' | 'pending', number>;
// Pick: select specific fields
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit: exclude specific fields
type PublicUser = Omit<User, 'role'>;
Custom Mapped Types
// Convert all methods to async versions
type Asyncify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
};
interface UserService {
getUser(id: string): User;
createUser(data: Partial<User>): User;
}
type AsyncUserService = Asyncify<UserService>;
// getUser(id: string): Promise<User>
// createUser(data: Partial<User>): Promise<User>
// Make specific keys required, rest optional
type RequireFields<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
type UserWithRequiredEmail = RequireFields<Partial<User>, 'email'>;
// { id?: string; name?: string; role?: ...; email: string }
// Deep partial (recursive)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type DeepPartialUser = DeepPartial<{
id: string;
address: {
street: string;
city: string;
zip: string;
};
}>;
// { id?: string; address?: { street?: string; city?: string; zip?: string } }
Key Remapping
// Rename keys
type PrefixKeys<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
type PrefixedUser = PrefixKeys<User, 'user'>;
// { userId: string; userName: string; userEmail: string; userRole: ... }
// Filter keys by value type
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStringFields = StringKeys<User>;
// { id: string; name: string; email: string }
// role is 'admin' | 'user' which extends string, so it's included
// If User had a number field, it would be excluded
Real-World Usage: Form Errors
interface LoginForm {
email: string;
password: string;
}
// Map form fields to their error messages
type FormErrors<T> = {
[K in keyof T]?: string;
};
type LoginErrors = FormErrors<LoginForm>;
// { email?: string; password?: string }
const [errors, setErrors] = useState<LoginErrors>({});
// TypeScript catches typos
setErrors({ emaill: 'Invalid email' }); // Error: 'emaill' is not a key of LoginForm
setErrors({ email: 'Invalid email' }); // OK
The Pattern to Remember
type Transform<T> = {
[K in keyof T]: /* transform T[K] here */;
};
Once you internalize this pattern, you'll reach for mapped types every time you catch yourself writing nearly-identical interfaces.
TypeScript patterns with advanced types, mapped types, and conditional types: Whoff Agents AI SaaS Starter Kit.
Top comments (0)