← Back
April 4, 2026NodeJS6 min read
TypeScript mapped types: transform any interface into what you need
Published April 4, 20266 min read
I had a User interface and needed to create a UserForm type, a UserUpdateRequest type, a UserResponse type, and a UserValidation type — all with different but related shapes. I was maintaining them manually and they kept drifting out of sync. Mapped types let you derive all of these from a single source of truth automatically.
The mapped type syntax
typescript
// { [K in keyof T]: transformation }
// For each key K in type T, produce a new key with a transformed value type
type MyPartial = { [K in keyof T]?: T[K] }; // Standard Partial
type MyRequired = { [K in keyof T]-?: T[K] }; // Remove optional
type MyReadonly = { readonly [K in keyof T]: T[K] }; // Add readonly
type Mutable = { -readonly [K in keyof T]: T[K] }; // Remove readonly
Generating form state from a model
typescript
interface User {
id: string;
email: string;
name: string;
age: number;
role: 'admin' | 'user';
}
// Form field state for each property
type FormField = {
value: T;
error: string | null;
touched: boolean;
};
// Automatically generate form state type for any model
type FormState = {
[K in keyof T]: FormField;
};
type UserFormState = FormState>;
// {
// email: { value: string; error: string | null; touched: boolean };
// name: { value: string; error: string | null; touched: boolean };
// age: { value: number; error: string | null; touched: boolean };
// }
function initFormState(initial: T): FormState {
return Object.fromEntries(
Object.entries(initial as Record).map(([key, value]) => [
key,
{ value, error: null, touched: false },
])
) as FormState;
}
Validation rules derived from a type
typescript
type Validator = (value: T) => string | null; // null = valid
// Generate a validation object type from any interface
type ValidationRules = Partial<{
[K in keyof T]: Validator;
}>;
// TypeScript ensures validator types match the field types
const userRules: ValidationRules = {
email: (value) => {
if (!value.includes('@')) return 'Invalid email';
return null;
},
age: (value) => {
if (value < 18) return 'Must be at least 18';
return null;
},
// id and name are optional — don't need validation rules
};
function validate(data: T, rules: ValidationRules): Partial> {
const errors: Partial> = {};
for (const [key, validator] of Object.entries(rules) as [keyof T, Validator][]) {
if (validator) {
const error = validator(data[key]);
if (error) errors[key] = error;
}
}
return errors;
}
Key remapping with as clause
typescript
// Remap keys while iterating
type Getters = {
[K in keyof T as `get${Capitalize}`]: () => T[K];
};
type UserGetters = Getters;
// {
// getId: () => string;
// getEmail: () => string;
// getName: () => string;
// getAge: () => number;
// getRole: () => 'admin' | 'user';
// }
// Filter keys using never
type OnlyStringValues = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStringFields = OnlyStringValues;
// { id: string; email: string; name: string; role: 'admin' | 'user' }
API response with metadata
typescript
// Wrap each resource type with API response metadata
type ApiResponse = {
data: T;
meta: {
timestamp: string;
requestId: string;
};
};
// Derive all API response types from your model types
type ApiResponses> = {
[K in keyof T]: ApiResponse;
};
interface Models {
user: User;
order: Order;
product: Product;
}
type Responses = ApiResponses;
// {
// user: ApiResponse;
// order: ApiResponse;
// product: ApiResponse;
// }
// When you add a new model, the response type appears automatically
Mapped types eliminate drift. When your User interface changes, all derived types (FormState, ValidationRules, Getters) update automatically. No manual synchronization, no out-of-sync type declarations hiding bugs. The investment in learning mapped types pays off in any codebase that has multiple related type representations of the same data.
Share this
← All Posts6 min read