In TypeScript, given `type Config = { name: string; db: { host: string; port: number } }`, what exactly does `Partial<Config>` make optional, and why does `cfg.db.host = "x"` still compile when `cfg` is typed `Readonly<Config>`?
answer
- the mapping runs exactly once
- T[P] is copied, never transformed
- one level of keys, nothing deeper
- readonly guards the slot, not the value
- like const on an object binding
basics
~20 sBoth utilities map only over Config's top-level keys. Partial<Config> makes name and db optional but leaves host and port inside db required, and Readonly<Config> marks the db property read-only without touching the object it points to.
solid answer
~40 sBoth are one level deep, because both copy each property type through unchanged. `Partial<Config>` is `{ name?: string; db?: { host: string; port: number } }` — `db` becomes optional, but its own property type is untouched, so if you supply `db` at all you must still supply both `host` and `port`. `Readonly<Config>` marks the top-level properties `readonly`, and `readonly` on `db` forbids *reassigning* `cfg.db`, exactly the way `const` forbids rebinding a variable. The object that `db` refers to is still typed `{ host: string; port: number }` with no modifiers, so writing to `cfg.db.host` is perfectly legal. Making it deep requires a recursive type of your own; there is no built-in deep version of either utility.
code
typescript · 17 linestype Config = {
name: string;
db: { host: string; port: number };
};
const whole: Partial<Config> = { db: { host: "localhost", port: 5432 } };
// @ts-expect-error port is still required inside db
const partialDb: Partial<Config> = { db: { host: "localhost" } };
const cfg: Readonly<Config> = {
name: "api",
db: { host: "localhost", port: 5432 },
};
cfg.db.host = "10.0.0.1"; // allowed: the nested object type is untouched
// @ts-expect-error the db property itself cannot be reassigned
cfg.db = { host: "10.0.0.1", port: 5432 };go deeper
Be able to say that these utilities change only the top-level properties of a type, and that a nested object inside them keeps its original required, mutable shape.
Explain why from the definition: the mapping runs once over keyof T and copies T[P] through unchanged. Then draw the parallel between readonly on a property and const on a binding.
Show what this breaks in a real service — a nested merge payload that either fails to typecheck or silently replaces a whole sub-object — and how you restructure the API so the type says what the endpoint really accepts.
Own the call on whether a codebase gets a generic deep transform at all. Weigh the cost of one recursive type that must handle arrays, class instances and cycles against explicitly modelled, per-level types.
## Why shallow is the inevitable consequence of the definition The definitions in TypeScript's `lib.es5.d.ts` explain the whole behaviour: ```typescript type Partial<T> = { [P in keyof T]?: T[P] }; type Readonly<T> = { readonly [P in keyof T]: T[P] }; ``` The body of each is `T[P]` — the original property type, copied through with no transformation. The mapping happens once, over `keyof T`, which for `Config` is the union `"name" | "db"`. There is no step that reaches inside the type of `db` and does the same thing again, so the nested object type comes out the far side byte-for-byte identical to how it went in. Shallowness is not a limitation someone forgot to fix; it is what a single-pass mapping means. ## What Partial<Config> actually gives you ```typescript type Config = { name: string; db: { host: string; port: number } }; // Partial<Config> is: // { name?: string; db?: { host: string; port: number } } const a: Partial<Config> = {}; // ok const b: Partial<Config> = { db: { host: "h", port: 5432 } }; // ok const c: Partial<Config> = { db: { host: "h" } }; // error: port missing ``` The practical reading is that `Partial<Config>` models *replace the whole db block or leave it out entirely*. It cannot express *patch one field inside db*, which is very often the thing you actually wanted when you reached for it. That mismatch is the real interview point: the type is easy to write and quietly says something different from what a nested update endpoint does. ## What Readonly<Config> actually gives you ```typescript // Readonly<Config> is: // { readonly name: string; readonly db: { host: string; port: number } } declare const cfg: Readonly<Config>; cfg.db.host = "10.0.0.1"; // allowed — db's own type has no modifiers cfg.db = { host: "h", port: 5432 }; // error — the db property is read-only ``` The `readonly` modifier constrains the *slot*, not the value in it. `cfg.db` is a property you may read but not write; the object it evaluates to has type `{ host: string; port: number }`, an ordinary mutable object type, and every write into it goes through that type rather than through `Readonly<Config>`. This is exactly the relationship a `const` binding has with the object it points at, and framing it that way is usually the fastest way to make it click for an interviewer. ## The type-layer caveat that comes with it Because `readonly` lives entirely in the checker, even the top-level guarantee is soft. Assign the same object to a second variable typed plain `Config` and the compiler will happily let you write to `name` through that alias — a `Readonly<T>` value is assignable to `T`, since read-only and mutable properties are compatible for assignment in this direction. Nothing is emitted, so nothing prevents it at run time. `Readonly<T>` is best described as a statement about *this reference*, not about the object. ## What to do instead There is no built-in deep variant of any of these utilities, and that is a deliberate call: a recursive transform has to decide what to do with arrays, functions, `Map`, `Date`, class instances and cyclic references, and there is no single answer that suits everyone. In practice you have three options, in ascending order of cost: 1. **Restructure the API.** Take a nested patch as `{ db: Partial<Config["db"]> }` and apply each level explicitly. The type then says what you mean, and merging is per-level code you can actually read. 2. **Name the intermediate type.** Define `type DbConfig = { host: string; port: number }` and hand out `Partial<DbConfig>` where a db patch is expected, rather than trying to make one utility describe two levels. 3. **Write a recursive transform** when the shape is genuinely deep and generic. That is its own topic, with its own pitfalls around arrays and non-plain objects. ## The trap to see coming The defect this creates in real code is silent: a service accepts `Partial<Config>` as a merge payload, someone sends only `{ db: { host: "newhost" } }`, and the compiler rejects it — or, worse, someone types the merge loosely, the object-level replacement drops `port`, and a required field vanishes at run time with the type still claiming it is a `number`. Knowing that both utilities stop at one level is what lets you predict which of those two you are about to get.
- So what type would you actually use for a request body that patches a single field inside `db`?Name the nested type and be explicit: `{ name?: string; db?: Partial<DbConfig> }` where `DbConfig` is `Config["db"]`. That says exactly what the endpoint accepts — an optional partial db block — and it forces the merge code to be written per level, which is what you want anyway, because a one-shot spread would replace the whole db object.
- If `Readonly<Config>` does not stop mutation, what is it worth?Quite a lot at the boundary it does cover. A parameter typed `Readonly<T>` documents and enforces that this function does not write to what it was handed, so a reviewer does not have to read the body to know that, and an accidental assignment fails the build. It is a local contract on one reference, not a property of the object.
- Does `Readonly<T>` behave the same way on an array type?A homomorphic mapped type over an array maps the element positions, so `Readonly<string[]>` gives you `readonly string[]` — no `push` or index assignment. But that only applies when the array *is* the type you map. `Readonly<{ tags: string[] }>` makes the `tags` property read-only while its value stays a mutable `string[]`, so `push` on it still compiles.
saying these in an interview costs you the question
- Expects Partial to make nested properties optional as well
- Thinks readonly on a property makes the referenced object immutable
- Believes a built-in DeepPartial or DeepReadonly ships with TypeScript
- Says a Readonly<T> value cannot be passed where T is expected
- Confuses reassigning a property with mutating the object it holds