DEV Community

Atlas Whoff
Atlas Whoff

Posted on

TypeScript Mapped Types: Transform Types Without Repetition

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';
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'>;
Enter fullscreen mode Exit fullscreen mode

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 } }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The Pattern to Remember

type Transform<T> = {
  [K in keyof T]: /* transform T[K] here */;
};
Enter fullscreen mode Exit fullscreen mode

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)