Articles
TypeScript Mapped Type Interview Questions: Key Remapping and Optional Modifiers
Practice TypeScript mapped type interviews with key remapping, optional and readonly modifiers, exact optional writes, and positive and negative tests.
An API record has a read-only identifier, a required name, and an optional nickname. You need an editable draft, a patch request, and generated getters. Reusing one clever utility for all three can accidentally remove a modifier, accept an undefined value, or rename a key into a collision.
Official type-system behavior: TypeScript mapped type interview questions are about property contracts: which keys exist, what values they permit, and whether presence and assignment rules survive the transformation. Start from an explicit input type, derive the output one property at a time, and test both allowed and rejected assignments. A successful compile is not runtime validation of a JSON response.
The official mapped-type reference supports the mechanics below. No candidate reports establish an employer's prompt format or question frequency here; the exercises and preparation recommendations are original reasoning.
What does a mapped type iterate over?
A mapped type creates properties by iterating over a union of property keys. For each key, it chooses the output key, value type, and property modifiers. It operates in the type system; it does not loop over object values or create properties in a running program.
The TypeScript mapped types handbook introduces the syntax. Use this original record throughout the exercise:
type Profile = {
readonly id: string;
name: string;
nickname?: string;
};
type Copy<T> = { [K in keyof T]: T[K] };
type ProfileCopy = Copy<Profile>;
keyof Profile supplies the key union. T[K] retrieves each associated value type, rather than replacing all values with one unrelated union. In this straightforward property-preserving mapping, the optional and readonly modifiers are preserved.
Explain one property at a time: id remains a read-only string, name remains required, and nickname remains optional. If your answer jumps directly to “it clones the object,” correct the terminology. It constructs a type, and no runtime object has been cloned.
How do optional and readonly modifiers change?
Mapped modifiers can add or remove property-level optionality and readonly status. Here the requirements are deliberately different:
type Draft<T> = {
-readonly [K in keyof T]: T[K]
};
type Complete<T> = {
[K in keyof T]-?: T[K]
};
type Patch<T> = {
[K in keyof T]?: T[K]
};
For the original profile, Draft permits reassignment of id but leaves nickname optional. Complete requires nickname while retaining the identifier's readonly modifier. Patch permits omission of every property; it does not inherently make the identifier writable or define how the server interprets a request.
The distinction matters in an interview because “make a draft” and “make every field optional” are not synonyms. Ask whether a draft may change identifiers at all. If not, omitting the identifier from an editable type is clearer than broadly removing readonly and hoping callers behave.
These transforms are shallow. A nested object can remain mutable through a readonly property, and a nested optional field does not become required because its parent did. Do not quietly promise deep immutability from a property-level modifier.
Does optional mean undefined is a valid write?
An optional property can be absent. Reading it may produce undefined. Whether assigning an explicit undefined is permitted depends on its declared value type and compiler settings; absence and a present property containing undefined are not identical object states.
TypeScript's exactOptionalPropertyTypes documentation makes this distinction explicit. With strict checking and that flag enabled, test:
type PatchName = { name?: string };
const absent: PatchName = {};
const present: PatchName = { name: 'Ada' };
// @ts-expect-error: present undefined was not declared
const invalid: PatchName = { name: undefined };
type Explicit = { name?: string | undefined };
const allowed: Explicit = { name: undefined };
The rejected assignment is a compile-time test, not a runtime exception. In a patch protocol, decide separately whether absence means leave unchanged, null means clear, and an empty string is an actual value. JSON serialization can also change which properties reach the server, so a static type cannot substitute for a wire-format contract.
Do not infer the original optional modifier solely by checking whether a read type includes undefined. A required property may explicitly allow undefined. Conversely, reading an optional property reflects possible absence. Test presence rules directly when the protocol depends on them.
How does key remapping create a different API?
Key remapping uses as inside a mapped type. Combine it with template literal types to produce a getter contract:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]:
() => T[K]
};
type ProfileGetters = Getters<Profile>;
Reason about the key transformation and return type separately. The source key name produces getName, whose function returns the property's read type. The string & K intersection excludes non-string key possibilities from the string operation; it does not stringify every possible numeric or symbol key.
The utility declares a shape, not generated implementations. A real factory still needs to create functions, capture the correct object, and handle missing fields. If you cast the factory result to Getters<T> without demonstrating those behaviors, you have moved the proof obligation rather than completed it.
In the compile fixture below, this mapping preserves an optional getNickname property; callers cannot assume that getter exists. Decide whether the getter itself should be optional or always present and return a possibly missing value. Do not rely on an unexamined utility when consumers require one specific behavior. Adding -? to the output declares required getters, but it does not make an absent source nickname magically exist.
How do you filter keys without losing their values?
Map an excluded key to never to remove it. For the original profile, a mutable patch should not accept id at all:
type EditablePatch<T> = {
-readonly [K in keyof T as
Exclude<K, 'id'>]?: T[K]
};
const update: EditablePatch<Profile> = { name: 'Lin' };
// @ts-expect-error: identifiers are not patch fields
const wrong: EditablePatch<Profile> = { id: 'new-id' };
Now explain the three independent actions: remove the identifier key, remove readonly on surviving properties, and allow omitted patch fields. Changing only the values cannot prevent a forbidden key from appearing in the declared shape.
An important limitation is structural assignability. An object literal checked against a target receives excess-property checking, but an existing variable with extra properties may still be assignable structurally. Do not claim the type proves that a runtime object contains no identifier. Use a runtime allowlist before constructing the actual patch request.
For value-based filtering, define the rule precisely. Does an optional string field qualify as a string field when its read type includes undefined? Should a union of string and number qualify? Those decisions can change the conditional used in as; the type's name should not hide the policy.
What happens to numeric keys, symbols, and collisions?
String remapping requires a plan for keys that are not strings. One contract may intentionally discard numeric and symbol keys. Another may preserve them unchanged while applying a prefix only to string keys. State which contract you are implementing before writing the conditional.
Try the original adversarial shape { URL: string; url: number }. Lowercasing both keys targets the same output spelling. A key transform is therefore not necessarily reversible or one-to-one. Our lowercase-key fixture accepts a string or number at the merged url property and rejects a boolean. That compile-time union does not choose a runtime winner. The runtime transformation needs an explicit collision policy: reject ambiguous input, choose a documented winner, or retain a separate mapping.
Do not assume runtime enumeration matches keyof perfectly. Inherited properties, symbols, and enumerable versus non-enumerable properties complicate object traversal. If you implement a transformer, constrain its accepted object domain and choose the enumeration primitive accordingly.
The interview skill is noticing that a concise type expression can describe an impossible or ambiguous runtime operation. Name the ambiguity before producing more syntax. That is especially useful when the task starts with an API schema but ends with an implementation requirement.
Why can a union make the utility surprising?
Suppose an API response is either { kind: 'user'; name: string } or { kind: 'team'; members: string[] }. A transform over a union must preserve the relationship between the discriminator and the fields belonging to that branch. Flattening values into unrelated unions can admit combinations the server never returns.
The indexed access types handbook supports reasoning about T[K]; the conditional types handbook explains distributive transformations. If the intended transform is branch-by-branch, make that intention explicit and compile examples for both branches.
Test a valid user, a valid team, a team missing its members, and a mismatched discriminator. Also test the utility outside the final union alias. Some failures become clearer when you inspect the intermediate key union or value type rather than one huge diagnostic.
Do not reach for any to make the example compile. An escape hatch can erase exactly the branch relationship the exercise is supposed to demonstrate. If you need a cast at a runtime boundary, explain which validation or invariant justifies it and keep that boundary narrow.
How should you verify a mapped-type answer?
Build a small compile-only fixture with a pinned TypeScript version and declared compiler flags. Include positive assignments and @ts-expect-error lines for negative cases. The latter should fail the test if a future edit makes a forbidden assignment silently legal.
For this article's schema, test identifier reassignment, missing required name, omitted nickname, explicit undefined, forbidden patch identifier, getter names, and a non-string source key. Use a separate configuration with exact optional checking disabled only to demonstrate the difference, not to hide errors in the primary contract.
Executed example evidence: The fixture was rerun with TypeScript 5.9.3, --noEmit --strict --exactOptionalPropertyTypes. Positive assignments compiled, and expected-negative cases remained errors: read-only reassignment, missing required fields, present undefined, a forbidden patch identifier, numeric getter keys, and mismatched union branches. Additional checks confirmed the merged collision value and the structural extra-property caveat. This pins a reproducible baseline rather than claiming that 5.9.3 is the newest release. It does not execute a getter factory or validate untrusted JSON.
Then test runtime validation independently. Feed the decoder a number where a string is required, a missing discriminator, and unexpected properties. A compile-only fixture cannot exercise those inputs because data from a network is not validated by a generic annotation.
For recursive utilities, add a termination and applicability discussion: arrays, tuples, functions, Date objects, and recursive schemas are not plain records with interchangeable treatment. State that the interview solution is shallow unless deeper behavior is actually implemented and tested.
Practice a concrete property contract
| Practice question | Topic-specific follow-up |
|---|---|
| Review and Improve Four AI-Assisted TypeScript Pull Requests | Review mapped utilities against accepted and rejected assignments. |
| Design a REST API Abstraction Layer | Separate optional wire fields from internal required state. |
| Selection-State Class for an Infinite-Scroll Checkbox List with Select All | Distinguish absent overrides from explicit state. |
| Implement a multi-step form in React | Model partial drafts and required submission fields. |
| Implement Recursive React JSON Viewer with Collapse | Explain the limits of recursive type transformations. |
For each question, write a small input/output schema and identify one property whose presence, ownership, or key changes. Follow it with an accepted assignment and a rejected assignment. That makes the exercise testable rather than a collection of utility names.
The final answer should separate three transformations
For TypeScript mapped type interview questions, explain keys, value types, and modifiers independently. Then show where the compiler contract ends and runtime validation begins. A compact utility is useful only when its optionality, non-string-key behavior, and forbidden-field rules match the application.
Use the TypeScript pull-request review task to inspect one mapped utility: record its keys, write rules, and presence rules, then defend a negative test before accepting the change.
Sources and Further Reading
- TypeScript: Mapped types
- TypeScript: Exact optional property types
- TypeScript: Template literal types
- TypeScript: Indexed access types
- TypeScript: Conditional types
Research checked October 1, 2026. Examples are original compile-time exercises. Compiler version and exactOptionalPropertyTypes settings must be recorded; type correctness does not establish runtime input validity.