In TypeScript, how does typing a value as `Readonly<T>` differ from writing `as const` on the object literal?
answer
- one is a type, one is an expression
- widening versus modifiers
- narrowest literal types, all the way down
- only one of them fits in a signature
- both erased, neither freezes anything
basics
~20 sReadonly<T> is a type operator applied to an existing type: it marks the top-level properties read-only and leaves the property types alone. A const assertion applies to a literal expression, infers the narrowest literal types, and makes the whole literal deeply read-only.
solid answer
~50 sThey work at different layers. `Readonly<T>` is a type-level transform you can apply anywhere a type goes — a parameter, a return type, a type alias — and it does one thing: mark the top-level properties `readonly`, keeping each property type exactly as declared. `as const` is an assertion on an *expression*, so it only works where you have a literal in hand, and it does two things at once: it infers literal types instead of widened ones, so `{ mode: "dark" }` gets `mode: "dark"` rather than `mode: string`, and it makes the literal read-only all the way down, turning nested array literals into readonly tuples. In practice I use `as const` to freeze a lookup table or a union source at the point of definition, and `Readonly<T>` in signatures to say a function will not write to what it was given. Both vanish at compile time.
code
typescript · 12 linesconst literal = { mode: "dark", tags: ["a", "b"] } as const;
// literal: { readonly mode: "dark"; readonly tags: readonly ["a", "b"] }
type Settings = { mode: string; tags: string[] };
const mapped: Readonly<Settings> = { mode: "dark", tags: ["a", "b"] };
// mapped.mode is string, and mapped.tags is still a mutable string[]
mapped.tags.push("c"); // allowed
// @ts-expect-error the mode property is read-only
mapped.mode = "light";
type Mode = typeof literal.mode; // "dark" — only the assertion keeps the literalgo deeper
Know that a const assertion goes next to a value while Readonly<T> goes where a type goes, and that the assertion also keeps literal types like "dark" instead of widening them to string.
Explain the three concrete differences — literal inference, depth, and where each one is even allowed to be written — and note that both are erased at compile time.
Show the judgment: const assertions at definition sites for tables and union sources, Readonly<T> in signatures to constrain callees. Call out that assignability to a mutable type quietly launders the guarantee either way.
Own the convention for the codebase — where read-only intent is expressed and how far it is expected to travel — knowing neither construct survives compilation and neither constrains data crossing a network or JavaScript boundary.
## Two different layers of the language The cleanest way to hold these apart is to notice *where each one can be written*. `Readonly<T>` is a generic type. It appears in type positions — in an alias, on a parameter, as a return type — and it takes a type in and gives a type out. It never appears next to a value. `as const` is a const assertion, and it appears in *expression* positions, attached to a literal you are writing right there. You cannot put it in a type alias, and you cannot apply it to a value that arrived from somewhere else, such as a function parameter or a parsed JSON payload. If you do not have a literal in your hand, `as const` is not available to you at all. That asymmetry alone answers most of the question, but the behavioural differences are what an interviewer is usually digging for. ## Difference one: inference width ```typescript const a = { mode: "dark" }; // a.mode is string const b = { mode: "dark" } as const; // b.mode is "dark" type Settings = { mode: string }; const c: Readonly<Settings> = { mode: "dark" }; // c.mode is string ``` By default TypeScript widens a string literal in a mutable property to `string`, on the reasonable assumption you might assign another string later. A const assertion suppresses that widening and keeps the literal type. `Readonly<T>` does nothing of the kind — it maps over a type that already exists, and `T[P]` is whatever it was declared to be. If `Settings` says `mode: string`, then `Readonly<Settings>` says `readonly mode: string`. The literal is gone before the utility ever sees it. This is why `as const` is the tool for deriving a union from a value: `typeof b.mode` is `"dark"`, and `(typeof arr)[number]` over an `as const` array gives you the union of its elements. `Readonly<T>` cannot produce a union out of thin air. ## Difference two: depth `Readonly<T>` maps once over `keyof T` and copies each property type through unchanged, so it is one level deep. A const assertion applies through the whole literal expression, so nested object literals become read-only too and nested array literals become readonly tuples: ```typescript const cfg = { name: "api", db: { host: "localhost", port: 5432 }, tags: ["a", "b"], } as const; // cfg.db.host is "localhost" and read-only // cfg.tags is readonly ["a", "b"] — no push, no index assignment type Cfg = { name: string; db: { host: string }; tags: string[] }; declare const m: Readonly<Cfg>; m.db.host = "x"; // allowed — nested type untouched m.tags.push("c"); // allowed — the property is read-only, the array is not ``` The `tags` line catches people out reliably. `Readonly<Cfg>` makes the *property* `tags` read-only, so you cannot swap in a different array; the value it holds is still a `string[]`, which has `push`. Note the separate case where the array *is* the mapped type: a homomorphic mapped type over an array maps its elements, so `Readonly<string[]>` genuinely is `readonly string[]`. ## Difference three: what you can apply it to ```typescript function render(opts: Readonly<Options>) { /* ... */ } // fine // there is no way to write `as const` in that parameter position ``` Only `Readonly<T>` can express "this function will not mutate its argument" in a signature. And only `as const` can pin down a literal you are authoring. They are not competitors so much as tools for two different jobs that happen to both involve the word "read-only". ## What they share Both are erased. Neither emits a single byte of JavaScript, neither calls any runtime freezing mechanism, and neither protects the object from code that did not go through the checker. A value built with `as const` is a plain, fully mutable JavaScript object once the program is running; the readonly-ness lived only in the compiler. Both are also assignable *out* to mutable types in the usual direction, so a `Readonly<T>` value can be passed where `T` is expected — which quietly launders the guarantee. The exception is arrays: a `readonly string[]` is not assignable to `string[]`, so the tuple half of an `as const` does hold its ground. ## The practical rule Use `as const` at the point of definition, on literals: lookup tables, route maps, permitted-value lists you want a union from. Use `Readonly<T>` in type positions, mostly signatures, to constrain what a function may do with a value it did not create. Reaching for `as const` and expecting it to constrain a parameter, or for `Readonly<T>` and expecting literal types, is the mistake this question is designed to surface.
- Which one would you use to derive a union type of allowed values, and why?The const assertion. Without it, an array literal of strings infers as `string[]` and a union derived from it collapses to `string`. With `as const` the array becomes a readonly tuple of literal types, so `(typeof VALUES)[number]` gives the union of the actual values. `Readonly<T>` operates on a type that has already been widened, so there is nothing left for it to recover.
- Can you apply a const assertion to a function parameter to stop the function mutating it?No — a const assertion is an expression-level assertion, so it can only be attached to a literal you are writing. A parameter type is a type position, and the tool that fits there is `Readonly<T>` (or `readonly T[]` for arrays). That is the clearest division of labour between the two.
saying these in an interview costs you the question
- Says as const and Readonly<T> are interchangeable spellings
- Expects Readonly<T> to narrow properties to literal types
- Thinks as const can be written in a type or parameter position
- Believes a const assertion freezes the object at run time
- Assumes Readonly on a property also locks the array it holds