TypeScript Mapped Types

Mapped types let you transform every property of an existing type into a new type using a single, reusable declaration.

What Are Mapped Types?

A mapped type walks over the keys of another type using the `[K in keyof T]` syntax and produces a new type with the same keys but transformed values. This lets you avoid repeating the same shape twice when you only need a variation of an existing interface, such as making every field optional or turning every value into a boolean flag.

A Basic Mapped Type

type Feature = {
  name: string;
  enabled: boolean;
  votes: number;
};

type FeatureFlags<T> = {
  [K in keyof T]: boolean;
};

const flags: FeatureFlags<Feature> = {
  name: true,
  enabled: true,
  votes: false,
};

console.log(flags);

Modifiers: readonly and optional

Mapped types can add or strip the `readonly` and `?` modifiers using the `+` and `-` prefixes. `-readonly` removes read-only-ness and `-?` removes optionality, which is useful when you need a fully mutable, fully required version of a type that was declared the opposite way.

Adding and Removing Modifiers

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

// Strip readonly and make every property required
type Mutable<T> = {
  -readonly [K in keyof T]-?: T[K];
};

type MutableConfig = Mutable<Config>;

const cfg: MutableConfig = { host: "localhost", port: 8080 };
cfg.host = "127.0.0.1"; // allowed now that readonly is removed
console.log(cfg);

Remapping Keys with as

Since TypeScript 4.1, mapped types can rename each key with an `as` clause, typically combined with template literal types. This is how libraries generate getter/setter names, event handler names, or prefixed variants of an existing type without hand-writing every property.

Remapping Keys

interface Person {
  name: string;
  age: number;
}

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

function makeGetters<T extends object>(obj: T): Getters<T> {
  const result = {} as Getters<T>;
  for (const key of Object.keys(obj) as (keyof T)[]) {
    const name = `get${String(key).charAt(0).toUpperCase()}${String(key).slice(1)}`;
    (result as Record<string, () => unknown>)[name] = () => obj[key];
  }
  return result;
}

const person: Person = { name: "Ada", age: 34 };
const getters = makeGetters(person);
console.log(getters.getName(), getters.getAge());
  • Partial<T> - makes every property optional
  • Required<T> - makes every property required
  • Readonly<T> - makes every property readonly
  • Record<K, T> - builds an object type from a key union and a value type
  • Pick<T, K> - keeps only the chosen keys
  • Omit<T, K> - drops the chosen keys
Utility typeWhat it doesExample
Partial<T>All properties become optionalPartial<User>
Required<T>All properties become requiredRequired<Options>
Readonly<T>All properties become readonlyReadonly<State>
Record<K, T>Builds an object type with keys K and values TRecord<string, number>
Note: Mapped types built directly over `keyof T` (called homomorphic mapped types) automatically preserve the original optional and readonly modifiers unless you explicitly add or remove them with + or -.
Note: When you remap keys dynamically at runtime, TypeScript can't verify the shape you build matches the mapped type exactly, so expect to lean on a controlled cast like `as Getters<T>` at the boundary rather than sprinkling `any` throughout the function.

Exercise: TypeScript Mapped Types

What does a mapped type such as `{ [K in keyof T]: T[K] }` fundamentally do?