TypeScriptMapped Types

Mapped Types

Mapped types let you create a new object type by iterating over the keys of an existing type and transforming them. Instead of copying a type manually, you describe the transformation once and TypeScript does the iteration for you.

They are the engine behind many of TypeScript's built-in utility types: Partial, Required, Readonly, Record, Pick, and Omit are all implemented as mapped types.

Basic Syntax

The fundamental form is:

type NewType = {
  [K in SomeUnion]: ValueType;
};

K is a local type variable that iterates over every member of SomeUnion. When SomeUnion is keyof T, you iterate over every key of type T.

TS
// Manually iterate over a union
type Flags = {
  [K in "read" | "write" | "execute"]: boolean;
};
// { read: boolean; write: boolean; execute: boolean }

// Iterate over keys of an existing type
interface User {
  id: number;
  name: string;
  email: string;
}

type UserFlags = {
  [K in keyof User]: boolean;
};
// { id: boolean; name: boolean; email: boolean }
Note
The [K in keyof T] syntax iterates over every key of T. The variable K holds each key's string literal type in turn.
Preserving Value Types

The most common use of mapped types is to keep each key but transform its value type. Accessing T[K] (an indexed access type) gives you the original value type for each key, so you can apply transformations while keeping the structure intact.

TS
interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

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

type ProductArrays = Arrayed<Product>;
// {
//   id: number[];
//   name: string[];
//   price: number[];
//   inStock: boolean[];
// }

// Make every value nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type NullableProduct = Nullable<Product>;
// { id: number | null; name: string | null; ... }
Adding Modifiers — Readonly and Optional

Mapped types can add (or remove) the readonly and ? modifiers during the iteration. The modifier goes before the key just like in a regular object type.

TS
// Make all properties readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties optional
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Both at once
type ReadonlyPartial<T> = {
  readonly [K in keyof T]?: T[K];
};

interface Config {
  host: string;
  port: number;
}

type FrozenConfig   = MyReadonly<Config>;        // { readonly host: string; readonly port: number }
type PartialConfig  = MyPartial<Config>;         // { host?: string; port?: number }
type SafeConfig     = ReadonlyPartial<Config>;   // { readonly host?: string; readonly port?: number }
Filtering Keys with never

When the value type resolves to never, TypeScript silently drops that key from the resulting type. This gives you a way to filter keys inside a mapped type.

TS
// Keep only keys whose value extends a given type
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  score: number;
}

type StringKeys  = PickByValue<Mixed, string>;  // { name: string }
type NumberKeys  = PickByValue<Mixed, number>;  // { id: number; score: number }

// The "as" clause is a key remapping — covered in depth in the next page.
// Here, returning never from "as" drops the key entirely.
Tip
The as clause in [K in keyof T as ...] remaps or filters keys. Returning never from it drops the key from the output type.
Built-in Utility Types as Mapped Types

Every major built-in utility type is a mapped type. Seeing their implementations cements your understanding of the pattern:

TS
// Partial<T> — makes all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Required<T> — removes optional modifier from all properties
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// Readonly<T> — makes all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Record<K, V> — creates an object type with keys K and value V
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

// Pick<T, K> — keeps only the listed keys
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Omit<T, K> — removes the listed keys
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Record — Building Object Types from Key Unions

Record<K, V> is a mapped type that does not iterate over an existing type — it iterates over a union K that you provide explicitly. This is useful for lookup tables, dictionaries, and status maps.

TS
type Status = "pending" | "active" | "closed";

// All statuses mapped to a label string
const statusLabels: Record<Status, string> = {
  pending: "Awaiting review",
  active:  "Currently active",
  closed:  "No longer active",
};

// All statuses mapped to a config object
type StatusConfig = Record<Status, { color: string; icon: string }>;

const config: StatusConfig = {
  pending: { color: "yellow", icon: "⏳" },
  active:  { color: "green",  icon: "✅" },
  closed:  { color: "gray",   icon: "🚫" },
};
Practical Example — Getters Generator

A mapped type can rename keys using the as clause combined with template literal types. Here is a utility that generates a getter-method interface for any data type:

TS
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserGetters = Getters<User>;
// {
//   getId:    () => number;
//   getName:  () => string;
//   getEmail: () => string;
// }

// You can implement it with a helper function
function makeGetters<T extends object>(obj: T): Getters<T> {
  const result: any = {};
  for (const key in obj) {
    const capitalized = key.charAt(0).toUpperCase() + key.slice(1);
    result[`get${capitalized}`] = () => obj[key];
  }
  return result;
}

const user = { id: 1, name: "Alice", email: "alice@example.com" };
const getters = makeGetters(user);
console.log(getters.getName()); // "Alice"
Recursive Mapped Types

Mapped types can be recursive, applying the transformation at every level of nesting. This is how you build deep utilities like DeepPartial or DeepReadonly:

TS
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface AppConfig {
  server: {
    host: string;
    port: number;
    tls: {
      enabled: boolean;
      certPath: string;
    };
  };
  debug: boolean;
}

type PartialConfig = DeepPartial<AppConfig>;

// Now every nested level is optional:
const partial: PartialConfig = {
  server: {
    host: "localhost",
    // port is optional
    tls: {
      // certPath is optional
      enabled: true,
    },
  },
  // debug is optional
};
Warning
Recursive mapped types can cause TypeScript to report "Type instantiation is excessively deep" for very deeply nested structures. Add a depth limit or use TypeScript 4.1+ recursive conditional types when this happens.
Mapped Types vs Index Signatures

Feature

Index Signature

Mapped Type

Syntax

{ [key: string]: V }

{ [K in keyof T]: T[K] }

Key constraint

string, number, or symbol

Any union or keyof T

Per-key value types

All keys share one type

Each key can have its own type

Key iteration source

Runtime keys

Compile-time type keys

Built-in utilities

No

Yes (Partial, Pick, etc.)

Homomorphic vs Non-Homomorphic Mapped Types

A mapped type is homomorphic when it iterates over keyof T directly. TypeScript then automatically preserves (or copies) the readonly and ? modifiers from the source type unless you explicitly change them.

A mapped type is non-homomorphic when it iterates over an independent key union (like in Record). No modifiers are carried over.

TS
// Homomorphic — modifiers are preserved
type Identity<T> = { [K in keyof T]: T[K] };

interface Source {
  readonly id: number;
  name?: string;
}

type Copied = Identity<Source>;
// { readonly id: number; name?: string }
// ✅ readonly and ? are preserved

// Non-homomorphic — no modifier preservation
type AllOptional<K extends string> = { [P in K]?: string };
type Mapped = AllOptional<"a" | "b">;
// { a?: string; b?: string }  — ? was added explicitly, not copied
Success
You now understand mapped types: iterating over keyof T, transforming value types, adding/removing modifiers, filtering with never, and all the built-in utility types that map types power.