TypeScript / Intermediate / Lesson 8
Mapped types and utility types
Derive related object views while preserving deliberate optionality and mutation policy, without confusing a type transformation with runtime object processing.
On this page
Lesson goals
- Map known keys and apply readonly or optional modifiers.
- Use Pick, Omit, Partial, Readonly, and Record deliberately.
- Implement real patch and serialization behavior separately.
Derive a family from one source model
Mapped types iterate over a key union to construct another object type. This can keep permission flags, selected views, or optional updates synchronized with a source model. The mapping happens in the checker and emits no loop that creates properties. If a runtime object is required, code must build it. Prefer a small named transformation with a clear purpose over a dense expression repeated across an API. Changing the source model may automatically change every derived view, which is useful only when those consumers genuinely should evolve together. Consider compatibility before exposing all keys of an internal representation.
Map property names into a different value policy
The first example maps notification channel keys to booleans. A missing known key or a non-Boolean flag would be rejected in this fresh literal. The runtime flags exist because the literal explicitly creates them, not because Flags runs at startup. A mapped type can also add or remove readonly and optional modifiers, with -readonly or -? expressing removal. Those operations affect the static view and do not freeze or populate objects. Keep finite key mappings distinct from an unbounded string dictionary whose arbitrary lookup may be absent.
export {};
type Flags<T> = { [K in keyof T]: boolean };
type Channels = { email: string; sms: string };
const flags: Flags<Channels> = { email: true, sms: false };
console.log(JSON.stringify(flags));
{"email":true,"sms":false}
The object contains the two explicitly created flags. The type enforces their relationship during checking only.
Use focused updates instead of whole-object permissions
Pick selects keys, Omit excludes keys, Partial makes selected properties optional, and Readonly restricts writes through a view. They are mostly shallow transformations. The second example permits only a name patch, leaving id outside the update type. With exact optional properties, an omitted name means no replacement; explicit undefined is not silently accepted as a string patch. The spread implementation makes a new top-level object and does not modify the source. An external patch must still be validated and authorized before reaching this typed helper. The type alone does not strip unexpected runtime fields.
export {};
type User = { id: number; name: string };
type NamePatch = Partial<Pick<User, "name">>;
function update(user: Readonly<User>, patch: NamePatch): User { return { ...user, ...patch }; }
const original: User = { id: 7, name: "Mira" };
const changed = update(original, { name: "Lina" });
console.log(JSON.stringify(changed), original.name);
{"id":7,"name":"Lina"} Mira
The new object changes name while preserving id. The original name remains Mira.
A transformed type is not a data transformer
Omit<PrivateUser, "secret"> does not delete secret from a value, and Readonly<T> does not recursively freeze it. Partial<T> is not a complete patch protocol because an application may distinguish leave unchanged, clear a value, and replace a value. Encode those operations explicitly when needed rather than overloading undefined or null without a contract. Record can model known finite keys or a dictionary, but neither manufactures entries. Test the implementation with extra properties arriving from unchecked JavaScript and reject or select fields before applying changes. Compile-time convenience must not become a substitute for runtime authorization or schema checking.
Keep derivation understandable to consumers
Utility types are valuable when they name a stable relationship, but long nested transformations can obscure error messages and expose accidental implementation detail. Export domain names for common input and output types, and test that their accepted shapes match intended calls. Key remapping can rename or filter keys, and template literal types can produce event names, which the next lesson develops. Avoid building a universal deep transformation before defining behavior for arrays, functions, optional fields, and cycles. Type-level recursion has compiler cost and can produce a model that looks precise while matching no real runtime operation. Prefer a smaller honest contract.
Common errors and fixes
Omit is expected to remove secret data.
- Why it happens
- A checker-only view was treated as runtime filtering.
- How to fix it
- Construct an explicit selected output object.
- Prevent it next time
- Verify serialized keys with runtime tests.
Partial updates accidentally erase values.
- Why it happens
- Omission and explicit clearing have no separate policy.
- How to fix it
- Model update operations and validate input before merging.
- Prevent it next time
- Test omitted, empty, null, and undefined cases intentionally.
A deep utility becomes hard to diagnose.
- Why it happens
- Complex recursion replaced a clear domain contract.
- How to fix it
- Use smaller named transformations and bounded requirements.
- Prevent it next time
- Measure checker cost and review error readability.
Check your understanding
1. Call update(original, {}). State the new value, whether identity is preserved, and which part of this behavior comes from the type.
Show worked solution
The new value has id seven and name Mira, but the spread creates a distinct top-level object. NamePatch permits omission; it does not itself perform copying. The implementation determines identity and shallow-copy behavior. If User later contains nested objects, their references remain shared unless a separate copy policy is implemented.
2. Design a patch that can explicitly clear a nullable nickname while leaving it unchanged when omitted. Explain why plain Partial is insufficient by itself.
Show worked solution
Use a documented optional nickname field whose allowed replacement is string | null: omission means unchanged, null means clear, and string means replace. Enable exact optional semantics and validate the external object before applying it. Partial can help express optionality, but the implementation and validation must enforce these distinct operations and reject unrelated fields.
Key takeaways
- Mapped types derive static relationships.
- Utility types do not copy, delete, or validate values.
- Define patch semantics before composing transformations.