Ch. 2 · TypeScript

TypeScript Mapped Types and Modifiers

How { [K in keyof T]: T[K] } rebuilds a type, how ?, readonly, -? and -readonly change it, and why Partial stops one level down.

~5 min readintermediateupdated Oct 7, 2026

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;
};
TypeScript

The simplest mapped type copies it:

type Copy<T> = { [K in keyof T]: T[K] };
TypeScript

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";
TypeScript
map1.ts(11,3): error TS2540: Cannot assign to 'id' because it is a read-only property.
Text

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 removed
TypeScript

The 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 };
TypeScript
map1.ts(18,7): error TS2741: Property 'email' is missing in type '{ id: string; name: string; nickname: undefined; }' but required in type 'Complete<Profile>'.
Text

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 };
TypeScript
map2.ts(3,58): error TS2322: Type 'undefined' is not assignable to type 'string'.
Text

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" };
TypeScript
map1.ts(28,7): error TS2741: Property 'email' is missing in type '{ id: string; }' but required in type 'NotHomomorphic'.
Text

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" };
TypeScript
map1.ts(25,57): error TS2353: Object literal may only specify known properties, and 'phone' does not exist in type 'Errors<Profile>'.
Text

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" } };
TypeScript
map2.ts(13,36): error TS2741: Property 'accent' is missing in type '{ mode: "dark"; }' but required in type '{ mode: "dark" | "light"; accent: string; }'.
Text

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] };
TypeScript

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] };
TypeScript

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.

Further reading

More in TypeScript

read ✓TypeScript · hard

TypeScript Key Remapping in Mapped Types

Rename keys with as clauses, drop keys by mapping to never, map over a union of events, and spot the string & K and optional-method traps.

~6 min readread →
read ✓TypeScript · mid

TypeScript Tuples for Fixed Position Data

When a fixed-length array beats an object, why a returned [value, setter] pair needs a type or as const, and the push hole in tuples.

~5 min readread →
esc