In TypeScript, how do you write a `Mutable<T>` mapped type that strips `readonly` from every property of `T`, and what do the `+` and `-` prefixes mean in a mapped type?
answer
- modifier gets the sign, not the key
- two signs, two modifiers
- plus is the silent default
- -readonly [K in keyof T]
- no built-in Mutable in lib.es5.d.ts
basics
~20 sWrite type Mutable<T> = { -readonly [K in keyof T]: T[K] }. In a mapped type the minus prefix strips a modifier the source property carried and plus adds one; both prefixes apply to readonly and to the optional marker ?.
solid answer
~40 sThe sign goes in front of the modifier, not the key: `type Mutable<T> = { -readonly [K in keyof T]: T[K] }`. Inside a mapped type both `readonly` and `?` can carry a `+` or a `-`. `+` adds the modifier and is exactly what a bare `readonly` or `?` already means, while `-` removes whatever the source property had. Mapping over `[K in keyof T]` copies the source property's modifiers across, which is what gives `-readonly` something to strip. TypeScript builds `Partial`, `Required` and `Readonly` on this same mechanism, but there is no built-in `Mutable`, so teams hand-roll it. It is shallow and purely compile-time: nested `readonly` members stay readonly, and the emitted JavaScript is unchanged.
code
typescript · 14 linestype Mutable<T> = { -readonly [K in keyof T]: T[K] };
interface Point {
readonly x: number;
readonly y: number;
}
const p: Mutable<Point> = { x: 1, y: 2 };
p.x = 10; // OK: the readonly modifier was stripped
const q: Point = { x: 1, y: 2 };
// q.x = 10; // Error: Cannot assign to 'x' because it is a read-only property
type MutableTuple = Mutable<readonly [number, number]>; // [number, number]go deeper
Be able to read a mapped type out loud: the part in brackets picks the keys, the part after the colon gives each property its type, and readonly or ? in front of or behind the bracket are modifiers.
Write Mutable<T> from memory with the minus in the right place, and explain that a bare modifier already means add while the minus removes what the mapping copied over from the source type.
Show where the helper stops helping: it is shallow, it is inert when the key set is not keyof T, and it changes nothing at runtime — so treat it as a scoped escape hatch, not as an immutability tool.
Own the convention: decide whether the codebase gets one shared Mutable helper or lets each module hand-roll one, and be clear that readonly annotations are a review aid rather than an enforced invariant.
## The shape of a mapped type A mapped type walks a set of keys and produces one property per key. The canonical identity mapping is: ```ts type Identity<T> = { [K in keyof T]: T[K] }; ``` Every property a mapped type produces can carry two modifiers: `readonly`, and the optional marker `?`. TypeScript lets you put an explicit sign in front of either one, and that sign is the whole subject here. ## The four spellings ```ts type AddReadonly<T> = { +readonly [K in keyof T]: T[K] }; // add readonly type DropReadonly<T> = { -readonly [K in keyof T]: T[K] }; // remove readonly type AddOptional<T> = { [K in keyof T]+?: T[K] }; // add ? type DropOptional<T> = { [K in keyof T]-?: T[K] }; // remove ? ``` The placement rule is simply that the sign hugs the modifier it controls. `readonly` sits before the `[K in ...]` bracket, so `-readonly` does too. The `?` sits after the closing bracket and before the colon, so `-?` does too. Writing the minus anywhere else is a syntax error, and this is the single most common thing candidates get wrong at the whiteboard. A bare modifier already means "add": `{ readonly [K in keyof T]: T[K] }` and `{ +readonly [K in keyof T]: T[K] }` denote the same type. The `+` form exists so that the two directions can be written symmetrically when you want the intent to be loud. ## Why there is anything to remove A mapping whose key set is written as `keyof T` — or a type parameter constrained to `keyof T` — copies each source property's modifiers onto the result. That copying is what makes `-readonly` meaningful: the mapping brings `readonly` across, and the minus then takes it back off. Contrast a mapping over an explicitly written key union: ```ts type OverLiterals = { [K in 'a' | 'b']-?: string }; // { a: string; b: string } ``` Here nothing was copied from a source type, the produced properties are required to begin with, and the `-?` has nothing to do. The minus is not an error, it is just inert. So when a `Mutable<T>` helper appears not to work, the first thing to check is whether the key set really came from `keyof T`. ## The built-in utilities are the same trick The standard library declares them in `lib.es5.d.ts` in exactly this form: ```ts type Partial<T> = { [P in keyof T]?: T[P] }; type Required<T> = { [P in keyof T]-?: T[P] }; type Readonly<T> = { readonly [P in keyof T]: T[P] }; ``` That is the reason interviewers like this question: reciting `Partial` is memorisation, but writing `Mutable` requires you to have actually read those three lines and understood that `-` is the inverse of the implicit `+`. Note the asymmetry in what ships: TypeScript gives you `Readonly` but no `Mutable`, and gives you `Required` (the `-?` direction) but no separate name for the `+?` direction beyond `Partial`. Every codebase that needs the readonly-stripping direction defines its own three-line helper. ## Arrays and tuples Because the mapping over `keyof T` is treated specially for array and tuple sources, the modifier removal reaches them too: ```ts type Mutable<T> = { -readonly [K in keyof T]: T[K] }; type A = Mutable<readonly string[]>; // string[] type B = Mutable<readonly [number, number]>; // [number, number] ``` This is the usual idiom for turning a tuple produced by `as const` back into something you can push to or sort in place. ## Two limits worth saying out loud First, it is **shallow**. `Mutable<T>` strips `readonly` from `T`'s own properties only; a property whose type is itself an object with `readonly` members keeps them. Reaching further down is a different technique entirely, and you should say so rather than pretend one level covers it. Second, it is **erased**. `readonly` and `?` exist only for the checker; the emitted JavaScript contains no property descriptors, no guards, and no trace of the modifier either way. `Mutable<T>` does not make anything mutable — the object was always mutable at runtime. What it changes is whether the compiler complains when you assign. That framing is what separates a candidate who has learned the syntax from one who understands what the type layer is. ## How to answer it in the room Write the three lines, say "the sign goes on the modifier", note that a bare modifier means add, point out that `Required` in the standard library is the `-?` version of the same idea, and close with the two limits. That is a complete answer in under a minute.
- Does removing `readonly` with `-readonly` change anything about the JavaScript that gets emitted?No. Property modifiers are part of the type layer and are erased during compilation, so the emitted JavaScript is byte-for-byte the same whether or not the mapped type stripped `readonly`. The object was always writable at runtime; `-readonly` only changes whether the checker rejects an assignment. A real runtime guarantee needs a runtime mechanism, not a modifier.
- Is `Mutable<T>` deep, and what does a nested object property look like after it?It is shallow. `Mutable<T>` maps only over `keyof T`, so a property whose type is `{ readonly id: string }` comes through with its own `readonly` intact — you can reassign the property itself but not the field inside it. Going deeper requires a different, recursive construction, and it is worth saying that explicitly rather than implying one level covers a whole object graph.
- Why does `-readonly` sometimes appear to do nothing?Usually because the mapping is not over `keyof T`. Modifiers are copied from the source properties only when the key set comes from `keyof T` or a parameter constrained to it; over an explicitly written key union like `[K in 'a' | 'b']` there was never a `readonly` to remove, so the minus is inert rather than broken. Check the key set before blaming the helper.
saying these in an interview costs you the question
- Thinks TypeScript ships a built-in Mutable utility type
- Writes readonly: false, expecting that to strip the modifier
- Puts the minus on the key instead of on the modifier
- Claims -readonly makes a frozen object writable at runtime
- Assumes -readonly recurses into nested object properties