Mapped Types

10 questions found

What is a mapped type, and what does the basic `{ [K in keyof T]: ... }` syntax mean?

Beginner
A mapped type generates a new object type by iterating over the keys of an existing type T (via `keyof T`) and applying a transformation to each property's type, letting you derive new types from existing ones without manually re-listing every property.
interface Person { name: string; age: number; }
type Stringify<T> = { [K in keyof T]: string };
type StringPerson = Stringify<Person>;
// { name: string; age: string }
Real-world example Deriving a form-values type where every field of a data model is represented as a string (matching raw form input).

Common follow-ups: What does 'keyof T' actually produce as a type on its own?

Index Signatures & Record Types

How does TypeScript's built-in Partial<T> utility type work, using mapped type syntax?

Beginner
Partial<T> is implemented as `{ [K in keyof T]?: T[K] }` — it maps over every key of T and adds a '?' to make each property optional, producing a version of T where every field can be omitted.
interface Person { name: string; age: number; }
type PartialPerson = Partial<Person>;
// { name?: string; age?: number }
function updatePerson(updates: PartialPerson) { /* ... */ }
Real-world example Typing an update/patch function's parameter where callers only need to supply the fields they want to change.

Common follow-ups: How would you implement the opposite of Partial — a Required<T> utility type?

Never Unknown & Void Types

How do the '+'/'-' modifiers work with '?' and 'readonly' in a mapped type?

Intermediate
By default, a mapped type COPIES existing modifiers ('?' and readonly) from the source type; explicitly writing `-?` or `-readonly` REMOVES that modifier from every mapped property, while `+?` or `+readonly` (equivalent to just writing them plainly) explicitly ADDS it — this is how built-in Required<T> and Writable-style utilities are implemented.
type Required<T> = { [K in keyof T]-?: T[K] }; // removes optionality
type Mutable<T> = { [K in keyof T]-readonly: T[K] }; // removes readonly

interface Config { readonly name?: string; }
type FullyMutableRequired = Mutable<Required<Config>>;
// { name: string }
Real-world example Building custom utility types that strip readonly or optionality from a type, mirroring built-in Required<T>.

Common follow-ups: What is the built-in Readonly<T> utility type's mapped-type implementation?

Readonly const Assertions & Immutability

How does 'key remapping' with the 'as' clause work in a mapped type?

Intermediate
Adding `as NewKeyExpression` after the `K in keyof T` clause lets you transform the resulting property NAME (not just its value type) — commonly combined with template literal types to systematically rename every key, like generating getter/setter method names from data properties.
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void
};
interface FormFields { name: string; age: number; }
type Handlers = EventHandlers<FormFields>;
// { onNameChange: (value: string) => void; onAgeChange: (value: number) => void }
Real-world example Auto-generating a consistent set of event-handler prop names from a data model, avoiding manual duplication and drift.

Common follow-ups: Can the 'as' clause also be used to conditionally OMIT a key entirely from the result?

Template Literal Types

How would you use a mapped type's 'as never' technique to filter out specific keys from a type based on their value type?

Intermediate
Combine key remapping with a conditional expression in the 'as' clause: for keys you want to KEEP, map to their normal name; for keys you want to EXCLUDE, map to 'never' — TypeScript automatically drops any property mapped to a 'never' key from the resulting type.
type OmitFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
};
interface Model { name: string; age: number; save(): void; }
type DataOnly = OmitFunctions<Model>;
// { name: string; age: number } -- 'save' is filtered out
Real-world example Deriving a plain data-only type from a class instance type, excluding its methods automatically.

Common follow-ups: How does this technique differ from using the built-in Omit<T, K> utility type with explicit key names?

Structural Typing & Duck Typing

How would you implement a deep 'DeepReadonly<T>' mapped type that recursively makes every nested property readonly?

Advanced
Combine a mapped type with a conditional type: for each key, check if its value is itself an object (and not a function or array edge case you want to treat specially); if so, recurse DeepReadonly into it, otherwise leave the primitive value type as-is — applied alongside the readonly modifier at every level.
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface Config { server: { host: string; port: number }; }
type FrozenConfig = DeepReadonly<Config>;
// server itself and server.host/server.port are all readonly
Real-world example Typing a fully immutable application state tree or configuration object, preventing accidental deep mutation anywhere.

Common follow-ups: Why does this DeepReadonly implementation need special handling to correctly deal with array or Map/Set properties?

Readonly const Assertions & Immutability

How do homomorphic mapped types (those that map directly over `keyof T` for a generic T) preserve extra information like optionality, compared to non-homomorphic ones?

Advanced
A homomorphic mapped type — one written as `{ [K in keyof T]: ... }` directly over a generic T — automatically preserves T's original modifiers (optional, readonly) and array/tuple structure unless explicitly overridden with +/-. A non-homomorphic mapped type (iterating over a fixed union of string literals instead of `keyof T`) does NOT get this automatic preservation, since there's no single source type to copy modifiers from.
// Homomorphic: preserves Person's original optional/readonly modifiers automatically
type Copy<T> = { [K in keyof T]: T[K] };

// Non-homomorphic: modifiers aren't inherited from any single source type
type FixedKeys = { [K in 'a' | 'b']: string };
Real-world example Understanding why some custom mapped-type utilities unexpectedly preserve readonly/optional modifiers while others don't.

Common follow-ups: How does this distinction affect a mapped type applied to a tuple or array type specifically?

Never Unknown & Void Types

How would you write a mapped type that converts every method on an interface into a Promise-returning async version, for a mock or proxy implementation?

Advanced
Use a conditional type inside the mapped type to detect function-typed properties specifically, and for those, produce a new function type with the same parameters but a Promise-wrapped return type — leaving non-function properties unchanged (or omitted, depending on the use case).
type Asyncify<T> = {
  [K in keyof T]: T[K] extends (...args: infer A) => infer R
    ? (...args: A) => Promise<R>
    : T[K]
};
interface Api { getUser(id: string): User; }
type AsyncApi = Asyncify<Api>;
// { getUser(id: string): Promise<User> }
Real-world example Generating a fully-typed async mock or remote-proxy version of a synchronous service interface automatically.

Common follow-ups: How would you handle methods that ALREADY return a Promise, to avoid double-wrapping (Promise<Promise<T>>)?

Function Types Overloads & Optional/Default Parameters

How do mapped types interact with union types when mapping over `keyof (A | B)`?

Advanced
`keyof (A | B)` produces only the INTERSECTION of keys common to both A and B (since only guaranteed-present keys are safe to access on either type), which can be a surprising and non-obvious result if you expected all keys from both types combined.
interface A { x: number; y: number; }
interface B { x: number; z: number; }
type Keys = keyof (A | B); // 'x' only — the only key guaranteed on both
Real-world example Debugging an unexpectedly narrow mapped type result, tracing it back to keyof behavior on a union rather than an intersection.

Common follow-ups: How does `keyof (A & B)` differ from `keyof (A | B)` in this respect?

Union & Intersection Types

How would you build a type-safe 'Builder' pattern using a mapped type that tracks which properties have already been set?

Advanced
Model the builder's state as a generic type parameter representing the set of already-set keys; each 'with' method returns a new builder type with that key added to the state (via a union or intersection update), and a 'build()' method is only made available (via a conditional type) once all required keys are present in the tracked state.
type Builder<T, Set extends keyof T = never> = {
  [K in keyof T]: (value: T[K]) => Builder<T, Set | K>
} & (Set extends keyof T ? { build(): T } : {});

// (Simplified illustration -- full implementation requires more advanced conditional plumbing)
Real-world example Designing a fluent configuration builder API that only allows calling .build() once every required field has actually been set, enforced at compile time.

Common follow-ups: Is this level of type-level state tracking common in real-world TypeScript codebases, or mostly a library-author technique?

Conditional Types