Typorithm · hard

5. Mapped types and key remapping

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

5. Mapped types and key remapping

0%

Concept and contract

Transform related fields systematically while preserving their individual value types.

A mapped type iterates over a key union and constructs related properties. The expression T[K] preserves each original field’s value type. This is a useful abstraction when multiple contracts must evolve together, such as form errors, change handlers or editable versions of a readonly record.

Start with a concrete two-field example before introducing the abstraction. If a manually written interface is clearer and does not drift, it may be sufficient. Mapped types should remove a real maintenance problem rather than hide a simple domain model behind unfamiliar syntax.

Mechanism and worked example

The example creates one optional error message per original field.

The example creates one optional error message per original field. The mapping iterates keyof T but deliberately changes every value to string. Another mapping could retain T[K], remove readonly with -readonly, or remove optionality with -? according to a different intended contract.

Key remapping uses as inside the mapping to rename or filter keys. Producing never for a key removes it. When building textual names, restrict keys to strings or handle number and symbol keys explicitly instead of assuming every property can be capitalized.

Complete worked example

typescript

type FieldErrors<T> = { [K in keyof T]?: string };
type Form = { name: string; count: number };
const errors: FieldErrors<Form> = { count: "Use an integer" };
Reasoning
The mapped key set follows Form while the output value type deliberately becomes string.

Counterexamples and common mistakes

The rejected error map uses a field that is absent from the source model.

The rejected error map uses a field that is absent from the source model. This catches a drifting form vocabulary. A Record<string, string> would accept the typo because it describes arbitrary keys rather than the finite set of supported form fields.

Mapped modifiers are static transformations. Removing readonly does not clone or unlock a frozen runtime object. Making a field required does not supply a default value. Runtime construction and validation must satisfy whatever stronger shape the new type promises.

Rejected counterexample: expected compiler diagnostic

typescript

type FieldErrors<T> = { [K in keyof T]?: string };
// @ts-expect-error typo is not a field in the source model.
const invalid: FieldErrors<{ name: string }> = { naem: "Missing" };
Reasoning
A finite mapping catches vocabulary drift that a string index signature would accept.

Independent practice

Build ChangeHandlers<T> that maps string keys to on plus a capitalized key plus Changed.

Build ChangeHandlers<T> that maps string keys to on plus a capitalized key plus Changed. Each callback should accept the corresponding T[K]. Begin with a concrete name and count model, then generalize only after you can write its two expected handlers manually.

Test that onCountChanged accepts a number while onNameChanged accepts text. Reject a wrong callback parameter type. Optional extension: filter out nonstring keys; this is additional practice and is not required before using the basic form-error mapping.

Step 1: Write the two concrete handler names. Step 2: Replace repetition with key remapping and T[K].

Practice starter: solve before reading the solution

typescript

type Form = { name: string; count: number };
// First write the two handler names and parameter types, then generalize.
type ChangeHandlers<T> = unknown;
Reasoning
Write the concrete handlers before replacing their repeated structure with a mapping.
  • Hint 1: Restrict textual key naming to string keys.
  • Hint 2: Do not widen each callback to the union of all values.

Solution, tests and trade-offs

The solution remaps K through a template literal and retains its corresponding value type in the callback.

The solution remaps K through a template literal and retains its corresponding value type in the callback. The string-key restriction avoids asking Capitalize to transform symbols. The result is a compile-time handler contract, not an automatically generated runtime event emitter.

Compile the handler object and invoke both callbacks with valid values. A negative callback assignment protects the key-value correlation. If a real runtime emitter is later added, its dispatch logic needs separate tests to ensure it calls the matching handler rather than merely satisfying a broad cast.

Practice solution and acceptance checks

typescript

type ChangeHandlers<T> = { [K in keyof T & string as `on${Capitalize<K>}Changed`]: (value: T[K]) => void };
const seen: string[] = [];
const handlers: ChangeHandlers<{ name: string; count: number }> = {
  onNameChanged: value => { seen.push(value); },
  onCountChanged: value => { seen.push(String(value)); }
};

// Acceptance checks
handlers.onNameChanged("QA");handlers.onCountChanged(2);if(seen.join(",")!=="QA,2")throw new Error("handlers");
Reasoning
Key remapping changes names while T[K] keeps each callback tied to its original field.

Interview reasoning and next step

For interviews, derive one mapped property at a time and explain which modifiers are preserved or changed.

For interviews, derive one mapped property at a time and explain which modifiers are preserved or changed. Show a concrete typo that the mapped contract catches. Avoid turning an ordinary form model into a recursive type exercise without a consumer need.

Continue with template literal types to understand the naming step independently. Keeping the operations separable makes both explanations and diagnostics easier to follow when a real API grows.

Optional stretch — after the core task works: Filter a symbol key out of the generated handler names.

Transform related fields systematically while preserving their individual value types.