Mapped types are how TypeScript builds one object type from another. Partial, Required, Readonly, Pick and Record are all mapped types, and they’re among the first utility types you’ll be asked to implement from scratch in an interview. The core syntax fits on one line. Most of the interesting questions are about modifiers: the ? and readonly flags, and what happens to them as a type passes through a mapping.
The loop at the heart of it
Here’s a type to work with, with one property of each interesting kind:
type Profile = {
readonly id: string;
name: string;
email?: string;
nickname: string | undefined;
};The simplest mapped type copies it:
type Copy<T> = { [K in keyof T]: T[K] };Read [K in keyof T] as a loop: for each key K in the union "id" | "name" | "email" | "nickname", create a property named K whose type is T[K], the type of that property in the original. It’s the type-level equivalent of Object.fromEntries(Object.keys(t).map(k => [k, t[k]])).
Copy<Profile> isn’t just structurally similar; it keeps id readonly and email optional:
declare const c: Copy<Profile>;
c.id = "x";map1.ts(11,3): error TS2540: Cannot assign to 'id' because it is a read-only property.That’s because it’s a homomorphic mapped type, one that maps over keyof T directly. TypeScript recognises the pattern and carries each property’s modifiers across. Remember that word, because the next sections depend on it.
Adding and removing modifiers
Inside the brackets you can add readonly before the key and ? after it. Prefix either with - to remove it, or + to add it explicitly (plain readonly and ? already mean add).
A draft for an edit form is a good example: every field optional, nothing readonly, so the form can fill it in gradually:
type Draft<T> = { -readonly [K in keyof T]?: T[K] };
const d: Draft<Profile> = {};
d.id = "u_1"; // ok: readonly removedThe reverse is a “complete” type where every field must be present:
type Complete<T> = { [K in keyof T]-?: T[K] };
const full: Complete<Profile> = { id: "1", name: "Ada", nickname: undefined };map1.ts(18,7): error TS2741: Property 'email' is missing in type '{ id: string; name: string; nickname: undefined; }' but required in type 'Complete<Profile>'.That’s exactly Required<T> from the standard library, and Draft is Partial plus the removal of readonly. Writing them yourself is a fair interview request, and so is explaining the difference between the two undefineds in Profile.
Optional versus “may be undefined”
email?: string and nickname: string | undefined look similar, but they’re different promises. The first says the key may be absent. The second says the key must be present, though its value may be undefined. You can see the difference in the error above: the object omitted email and got an error, but it included nickname: undefined and that was fine.
-? treats them differently too. Removing the ? from email also removes the undefined that the ? had added, so in Complete<Profile> the type of email is plain string:
const full2: Complete<Profile> = { id: "1", name: "Ada", email: undefined, nickname: undefined };map2.ts(3,58): error TS2322: Type 'undefined' is not assignable to type 'string'.But nickname keeps string | undefined, because that undefined was part of the declared type, not something added by a modifier. If you want to strip it as well, apply NonNullable<T[K]> to the value. Candidates often assume Required does that; it doesn’t.
When mappings lose modifiers
Map over a list of keys instead of keyof T and the type is no longer homomorphic:
type NotHomomorphic = { [K in "id" | "email"]: Profile[K] };
declare const nh: NotHomomorphic;
nh.id = "changed"; // ok: readonly is gone
const nh2: NotHomomorphic = { id: "1" };map1.ts(28,7): error TS2741: Property 'email' is missing in type '{ id: string; }' but required in type 'NotHomomorphic'.id lost its readonly and email became required. TypeScript only knows where to copy modifiers from when the key source is keyof of some type. The standard Pick avoids this by declaring its key parameter as K extends keyof T and mapping [P in K], which TypeScript also treats as homomorphic. Swap NotHomomorphic for Pick<Profile, "id" | "email"> and both modifiers survive: assigning to id gives the same TS2540 error, and omitting email is allowed.
This matters when you write helpers that pick a subset of fields from a domain type. Build them on Pick (or with K extends keyof T) rather than on a hard-coded union, or you’ll quietly turn readonly fields mutable.
Changing the value type
The value side of a mapped type doesn’t have to be T[K]. Validation errors for a form are a natural fit:
type Errors<T> = { [K in keyof T]?: string };
const errs: Errors<Profile> = { email: "Invalid email", phone: "x" };map1.ts(25,57): error TS2353: Object literal may only specify known properties, and 'phone' does not exist in type 'Errors<Profile>'.Every key of Profile may have an error message, and nothing else may. Rename a field in Profile and the error type follows. The same idea gives you { [K in keyof T]: boolean } for “touched” flags, or { [K in keyof T]: (value: T[K]) => string | undefined } for per-field validators, where each validator’s parameter type matches its field.
If you also need to rename keys, for instance to produce getName and getEmail, that’s the as clause covered in the key remapping note.
Partial is shallow
A common bug with settings objects:
type Settings = { theme: { mode: "light" | "dark"; accent: string }; locale: string };
const patch: Partial<Settings> = { theme: { mode: "dark" } };map2.ts(13,36): error TS2741: Property 'accent' is missing in type '{ mode: "dark"; }' but required in type '{ mode: "dark" | "light"; accent: string; }'.Partial makes theme optional, but if you provide theme, it must be complete. Mapped types only touch the top level. A recursive version handles nesting:
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };With that, the patch compiles. Production versions add cases for arrays, functions, Date and Map, since T[K] extends object is true for all of them and you usually don’t want a “partial function”. This is a common interview follow-up: write DeepPartial, then explain what it does to an array property. With this version, string[] becomes an object with optional numeric keys and optional array methods, which isn’t what anyone wants, so you’d check for arrays first and map their element type instead.
Interview practice
Implement Mutable<T>, the opposite of Readonly<T>. Then a colleague uses it on Pick<Profile, "id"> and on { [K in "id"]: Profile[K] } and gets the same result. Explain why the second one didn’t need Mutable at all.
Answer
type Mutable<T> = { -readonly [K in keyof T]: T[K] };It’s homomorphic, so it keeps ? on optional properties, and -readonly strips the readonly flag from every property. Applied to Pick<Profile, "id">, which kept readonly id because Pick is homomorphic, it produces { id: string } with id writable.
The second type, { [K in "id"]: Profile[K] }, maps over a literal union rather than keyof of a type, so it never copied the readonly modifier in the first place. id was already writable before Mutable touched it, which is why the results match. The point to make is that modifier preservation depends on the form of the mapping, and that hand-written key lists silently drop both readonly and ?. In real code I’d use Pick for subsets so the original modifiers survive, and apply Mutable only where I deliberately want a writable copy, such as a form draft.