TypeScript mapped types: transform any interface into what you need
← 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