Skip to main content

TS Ep 38: Mapped Types (Basics)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 38: This Article
If Array.prototype.map() is the “for-loop” of Value Space, Mapped Types are the “for-loops” of Type Space. They allow you to iterate over a union of string literals to programmatically construct a new object shape.

1. The Syntax: [K in Union]
#

A Mapped Type loops over a union of strings (or numbers/symbols) and constructs an object where those strings become the property keys.

type FeatureFlags = "darkMode" | "newDashboard" | "betaAccess";

// 🟢 Mapped Type!
// "For every key 'K' in the FeatureFlags union, the value is boolean"
type AppFeatures = {
  [K in FeatureFlags]: boolean;
};

/* Inferred Type:
{
  darkMode: boolean;
  newDashboard: boolean;
  betaAccess: boolean;
}
*/

(Note: This performs the exact same operation as the Record<K, V> utility type. In fact, Record is defined using this exact Mapped Type syntax under the hood!)


2. Deriving and Transforming Existing Objects
#

Mapped Types shine when combined with the keyof operator to iterate over the keys of an existing object interface.

Suppose we have a raw User interface, and we want to generate a new interface where every property is transformed into a “Getter” function returning the original property’s type.

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

// 1. `keyof User` produces the union: "id" | "name" | "age"
// 2. `K in ...` iterates over that union.
// 3. `User[K]` looks up the original property type using Indexed Access.
type UserGetters = {
  [K in keyof User]: () => User[K];
};

/* Inferred Type:
{
  id: () => string;
  name: () => string;
  age: () => number;
}
*/

const getters: UserGetters = {
  id: () => "usr_99",
  name: () => "Alice",
  age: () => 30,
};

3. Strict State Validation Maps
#

Mapped types are critical for creating “Validation Maps” or “Form State Managers” that must remain perfectly synchronized with a primary domain model.

Imagine you are writing a form library. For a given FormState, you need a configuration object providing a validation function for every single field:

interface RegistrationForm {
  username: string;
  email: string;
  age: number;
}

// 🟢 The Validator Map dynamically mirrors the FormState shape:
type ValidatorMap<T> = {
  [K in keyof T]: (val: T[K]) => boolean;
};

// We apply the mapped type to our RegistrationForm:
const registrationValidators: ValidatorMap<RegistrationForm> = {
  username: (val) => val.length >= 3,
  email: (val) => val.includes("@"),
  age: (val) => val >= 18,
};

The Architectural Benefit
#

If a backend engineer later updates the RegistrationForm interface by adding a new field:

interface RegistrationForm {
  username: string;
  email: string;
  age: number;
  phoneNumber: string; // <-- NEW FIELD ADDED
}

The Mapped Type ValidatorMap<RegistrationForm> instantly updates to require a phoneNumber validator.

TypeScript will immediately throw a compilation error on the registrationValidators object because it is missing the phoneNumber property! This guarantees your validation logic never falls out of sync with your data models.


4. Conditional Value Types
#

Because the value side of a mapped type evaluates for each key individually, you can use conditional types (which we cover deeply in the Advanced Module) to change the value type based on the key name!

interface DatabaseRecord {
  id: string;
  payload: string;
  createdAt: string;
}

// If the key is 'id', the value is readonly. Otherwise, it's mutable.
type MaskedRecord = {
  [K in keyof DatabaseRecord]: K extends "id" ? symbol : DatabaseRecord[K];
};

/* Inferred Type:
{
  id: symbol;          <-- Transformed!
  payload: string;     <-- Preserved
  createdAt: string;   <-- Preserved
}
*/

Summary & Next Steps
#

In this episode:

  • We learned the syntax for Mapped Types: [K in Union]: Type.
  • We derived new objects from existing interfaces using [K in keyof T]: () => T[K].
  • We built a strict ValidatorMap<T> that enforces exhaustive key mapping for form configurations.
  • We demonstrated how mapped types prevent domain models and utility objects from falling out of sync.

In Episode 39: Mapped Types (Modifiers), we will learn how to add and remove readonly and optional ? modifiers dynamically during the mapping loop!

typescript - This article is part of a series.
Part 38: This Article