Readonly, const Assertions & Immutability
10 questions found
How does the 'readonly' modifier on an interface property differ from using 'const' for a variable?
Beginner
'const' prevents REASSIGNING a variable binding itself, but doesn't stop you from mutating an object it points to; 'readonly' on a property prevents REASSIGNING that specific property after the object is created, while the object binding itself might still be a regular mutable 'let' or 'const' variable.
interface Point { readonly x: number; readonly y: number; }
const p: Point = { x: 1, y: 2 };
// p.x = 5; // Error: cannot assign to 'x' because it is a read-only property
p.x; // fine to read
Real-world example
Preventing accidental mutation of a coordinate, configuration, or value object's fields after construction.
Common follow-ups: Does 'readonly' provide any actual runtime enforcement, or is it purely compile-time?
Structural Typing & Duck Typing
How do you make an entire array type read-only, preventing both element mutation and array-mutating methods?
Beginner
Prefix the array type with 'readonly', or use the generic `ReadonlyArray<T>` — both prevent index assignment AND remove access to mutating methods like push, pop, splice, and sort from the type entirely.
const nums: readonly number[] = [1, 2, 3];
// nums.push(4); // Error: 'push' does not exist on type 'readonly number[]'
// nums[0] = 99; // Error: index signature is read-only
Real-world example
Typing a function parameter that should be able to read a caller's array but must never accidentally mutate it.
Common follow-ups: Which specific array methods remain available on a readonly array type, versus which are removed?
Function Types
Overloads & Optional/Default Parameters
What does the 'as const' assertion do when applied to an object literal?
Beginner
'as const' locks every property to its most specific literal type (rather than the general widened type like 'string' or 'number') AND makes every property (recursively) readonly, essentially treating the object literal as an immutable, precisely-typed constant.
const config = { theme: 'dark', maxRetries: 3 } as const;
// theme: 'dark' (literal), not string
// maxRetries: 3 (literal), not number
// config.theme = 'light'; // Error: readonly
Real-world example
Defining a precise, immutable configuration object whose exact literal values should be preserved in the type system.
Common follow-ups: Without 'as const', what wider types would 'theme' and 'maxRetries' have been inferred as instead?
Structural Typing & Duck Typing
Is 'readonly' at the type level enforced at runtime, and how can a readonly property still be mutated via a workaround?
Intermediate
'readonly' is a compile-time-only restriction, completely erased from the emitted JavaScript — at runtime, the property is a perfectly normal, mutable value, so any code that bypasses the type checker (like a type assertion, plain JavaScript caller, or Object.assign) can still mutate it without any runtime error.
interface Point { readonly x: number; }
const p: Point = { x: 1 };
(p as { x: number }).x = 99; // bypasses readonly via a type assertion, mutates fine at runtime
console.log(p.x); // 99
Real-world example
Understanding that 'readonly' is a development-time safety net, not a genuine runtime immutability guarantee like Object.freeze().
Common follow-ups: How would you combine 'readonly' with Object.freeze() to get BOTH compile-time and runtime immutability?
Never
Unknown & Void Types
How does 'as const' behave differently when applied to an array literal versus a plain object literal?
Intermediate
Applied to an array literal, 'as const' converts it from a general array type (like number[]) into a fixed-length, readonly TUPLE type with each element's specific literal type preserved — the same underlying mechanism as with objects, but the array-specific result is a tuple rather than just a 'more precise object'.
const colors = ['red', 'green', 'blue'] as const;
// inferred as: readonly ['red', 'green', 'blue']
// NOT string[], and NOT a mutable array
Real-world example
Deriving a precise union type of allowed values directly from an array of literal constants, often combined with 'typeof' and indexed access.
Common follow-ups: How would you derive a union type like 'red' | 'green' | 'blue' FROM this const-asserted array automatically?
Primitive Types
Arrays & Tuples
How do you combine Object.freeze() with TypeScript's readonly to get genuine runtime immutability that also satisfies the type system?
Intermediate
TypeScript's Object.freeze() type definition already returns a `Readonly<T>`-typed result, so calling it both enforces ACTUAL runtime immutability (attempting to mutate silently fails in non-strict mode, or throws in strict mode) AND correctly reflects that immutability in the type system simultaneously.
const config = Object.freeze({ theme: 'dark', retries: 3 });
// config is typed as Readonly<{ theme: string; retries: number }>
// config.theme = 'light'; // Error at compile time AND fails/throws at runtime
Real-world example
Creating a genuinely immutable shared constant object (like application configuration) with both compile-time and runtime protection.
Common follow-ups: Does Object.freeze() perform a deep freeze, or only a shallow one — and what does that mean for nested objects?
Structural Typing & Duck Typing
How would you implement a 'DeepReadonly<T>' utility type, and why doesn't the built-in 'Readonly<T>' recurse into nested objects?
Advanced
The built-in Readonly<T> is a simple, SHALLOW mapped type (`{ readonly [K in keyof T]: T[K] }`) that only adds the modifier at the top level; a true deep version requires a recursive conditional type that checks whether each property's value is itself an object and, if so, recursively applies DeepReadonly to it as well.
type DeepReadonly<T> = T extends (infer U)[]
? readonly DeepReadonly<U>[]
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config { server: { port: number } }
type Frozen = DeepReadonly<Config>; // server AND server.port are both readonly
Real-world example
Typing an immutable, deeply-nested global state tree or configuration object used throughout an application.
Common follow-ups: Why might a naive DeepReadonly implementation behave incorrectly on special object types like Map, Set, or Date?
Mapped Types
How does 'as const' interact with function calls, and why can't you apply it directly to a function's return expression the way you can to a variable's literal value?
Advanced
'as const' only works on literal EXPRESSIONS (object/array/primitive literals) written directly at that position — it can't retroactively make a value that's ALREADY been computed by a function call more specific, since the assertion needs to see the actual literal syntax to lock in literal types; wrapping the literal INSIDE the function (and letting the function's return type reflect that) is the correct approach instead.
function getConfig() {
return { theme: 'dark' } as const; // correct: applied directly to the literal being returned
}
// NOT: const result = getConfig() as const; -- this wouldn't add any extra narrowing beyond what getConfig() already returns
Real-world example
Designing factory functions whose return type should preserve literal types, by applying 'as const' at the actual literal construction site.
Common follow-ups: How would you type a factory function to return a precise literal-typed object without needing to manually repeat 'as const' at every call site?
Function Types
Overloads & Optional/Default Parameters
How would you derive a union type of allowed string values directly from a readonly array of literals, using 'typeof' and indexed access together?
Advanced
Combine 'as const' (to get a readonly tuple of literals) with `typeof arrayName[number]` — the `[number]` indexed access extracts the union of ALL element types in the tuple, giving you a derived union type that automatically stays in sync if the array's contents change.
const THEMES = ['light', 'dark', 'system'] as const;
type Theme = typeof THEMES[number]; // 'light' | 'dark' | 'system'
function setTheme(theme: Theme) { /* ... */ }
setTheme('dark'); // OK
// setTheme('blue'); // Error: not assignable to Theme
Real-world example
Keeping a runtime array (used for iteration, like populating a dropdown) and its corresponding literal union type automatically in sync from a single source of truth.
Common follow-ups: Why is this pattern generally preferred over maintaining a separate array AND a separate manually-written union type?
Union & Intersection Types
How does 'readonly' modify the variance/assignability rules between array types compared to their mutable counterparts?
Advanced
A `readonly T[]` is CONTRAvariant-friendly in a way mutable arrays aren't: a `Dog[]` IS assignable to `readonly Animal[]` (since you can only READ from it, so treating dogs as animals is always safe), but a `Dog[]` is NOT safely assignable to a mutable `Animal[]` (since someone could then push a Cat into what's actually a Dog array) — TypeScript actually allows the unsound mutable case too for practical reasons, but the readonly version is the genuinely SOUND direction.
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
const dogs: Dog[] = [{ name: 'Rex', breed: 'Lab' }];
const readonlyAnimals: readonly Animal[] = dogs; // sound and safe: read-only, so no invalid inserts possible
const mutableAnimals: Animal[] = dogs; // TypeScript allows this too, but it's technically unsound
Real-world example
Understanding a subtle but real class of type-safety loophole in TypeScript's array covariance rules, and why 'readonly' closes part of it.
Common follow-ups: Why does TypeScript still allow the unsound mutable-array assignment despite readonly being the technically correct direction?
Structural Typing & Duck Typing