TypeScript Mapped Types

A mapped type creates a new type by transforming the properties of an existing type. Instead of writing a new type from scratch, a mapped type iterates over the keys of an existing type and applies a transformation — making all properties optional, readonly, or changing their value types. Mapped types are the engine behind many of TypeScript's built-in utility types.

The Concept

  Original type:            Mapped type (all properties optional):
  +----------+----------+   +----------+-----------+
  | name     | string   |   | name?    | string    |
  | age      | number   |   | age?     | number    |
  | email    | string   |   | email?   | string    |
  +----------+----------+   +----------+-----------+
  Every property required    Every property optional

Basic Mapped Type Syntax

  { [K in keyof T]: TransformedType }
    |      |              |
    |      |              +-- New type for each property
    |      +----------------- Iterate over all keys of T
    +------------------------ K = current key name
// Make every property optional
type Optional<T> = {
    [K in keyof T]?: T[K];
};

type User = {
    name: string;
    age: number;
    email: string;
};

type PartialUser = Optional<User>;
// { name?: string; age?: number; email?: string }

let u: PartialUser = { name: "Rohini" }; // Valid — other fields optional

Making All Properties Readonly

type Immutable<T> = {
    readonly [K in keyof T]: T[K];
};

type Config = {
    host: string;
    port: number;
    debug: boolean;
};

type FrozenConfig = Immutable<Config>;
// { readonly host: string; readonly port: number; readonly debug: boolean }

let cfg: FrozenConfig = { host: "localhost", port: 3000, debug: false };
cfg.host = "example.com"; // Error: Cannot assign to 'host' — readonly

Making All Properties Required

// Remove the optional marker with -?
type Required<T> = {
    [K in keyof T]-?: T[K];
};

type DraftForm = {
    name?: string;
    email?: string;
    phone?: string;
};

type SubmittedForm = Required<DraftForm>;
// { name: string; email: string; phone: string }  ← all required now

Removing Readonly

// Remove readonly modifier with -readonly
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

type FrozenPoint = {
    readonly x: number;
    readonly y: number;
};

type MutablePoint = Mutable<FrozenPoint>;
// { x: number; y: number }  ← writable now

Mapping to a Different Type

// Convert all property values to boolean flags
type Flags<T> = {
    [K in keyof T]: boolean;
};

type Student = {
    name: string;
    age: number;
    grade: string;
};

type StudentFlags = Flags<Student>;
// { name: boolean; age: boolean; grade: boolean }

// Useful for tracking which fields have been edited
let editedFields: StudentFlags = {
    name: true,
    age: false,
    grade: true
};

Key Remapping (as Clause)

TypeScript 4.1 introduced the as clause inside mapped types for renaming keys during mapping.

// Prefix every property name with "get"
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Person = {
    name: string;
    age: number;
};

type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

Filtering Keys with Key Remapping

// Keep only string-value properties
type StringOnly<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type Product = {
    id: number;
    name: string;
    price: number;
    category: string;
    active: boolean;
};

type StringFields = StringOnly<Product>;
// { name: string; category: string }

Nested Mapped Types

// Make all nested properties optional too (deep partial)
type DeepPartial<T> = {
    [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type CourseConfig = {
    title: string;
    settings: {
        maxStudents: number;
        allowDownload: boolean;
        duration: {
            hours: number;
            minutes: number;
        };
    };
};

type DraftCourse = DeepPartial<CourseConfig>;
// All properties, including nested ones, become optional

let draft: DraftCourse = {
    title: "TypeScript Advanced",
    settings: {
        maxStudents: 30
        // allowDownload and duration can be omitted
    }
};

Mapped Type with Value Transformation

// Wrap every property in an array
type ArrayValues<T> = {
    [K in keyof T]: T[K][];
};

type Score = {
    math: number;
    science: number;
    english: number;
};

type ScoreHistory = ArrayValues<Score>;
// { math: number[]; science: number[]; english: number[] }

let history: ScoreHistory = {
    math:    [85, 90, 78],
    science: [88, 92],
    english: [75, 80, 85, 70]
};

Built-In Utility Types That Use Mapping

Utility TypeWhat It DoesEquivalent Mapped Type
Partial<T>All properties optional[K in keyof T]?: T[K]
Required<T>All properties required[K in keyof T]-?: T[K]
Readonly<T>All properties readonlyreadonly [K in keyof T]: T[K]
Record<K, V>Object with keys K and values V[P in K]: V

Practical Example

// Form state management with mapped types
type FormState<T> = {
    values: T;
    touched: { [K in keyof T]: boolean };
    errors: { [K in keyof T]: string | null };
    dirty: { [K in keyof T]: boolean };
};

type LoginForm = {
    email: string;
    password: string;
};

let loginState: FormState<LoginForm> = {
    values: {
        email: "",
        password: ""
    },
    touched: {
        email: false,
        password: false
    },
    errors: {
        email: null,
        password: null
    },
    dirty: {
        email: false,
        password: false
    }
};

// User types in the email field
loginState.values.email = "user@estudy247.com";
loginState.touched.email = true;
loginState.dirty.email = true;

// Validate
if (!loginState.values.email.includes("@")) {
    loginState.errors.email = "Invalid email address";
}

console.log(loginState.values.email); // user@estudy247.com
console.log(loginState.errors.email); // null

Leave a Comment