Question Details

No question body available.

Tags

typescript

Answers (1)

July 10, 2026 Score: 2 Rep: 341,561 Quality: Medium Completeness: 80%

For better or worse, TypeScript allows mutual assignability between readonly properties and regular, mutable properties. That is, a value of type T can be assigned both to and from a value of type Readonly. If you have an object, you cannot directly re-assign a readonly property, but you can just assign that object to a type without readonly and modify it that way. There is a longstanding feature request at microsoft/TypeScript#13347 to change this, and an even older request at microsoft/TypeScript#13002 which was originally titled something like "readonly is a joke". Some people would like to see it change so that Readonly is not assignable to T, but it's unlikely to ever actually happen. So nothing you do will actually enforce this properly. Any attempt will either end up preventing both writing and reading in some cases, or will not actually prohibit writing. It's even worse that that, since property writes are unsafe anyway in TypeScript. If you have a value of type {x: string} and assign it to a variable of type {x: string | number}, suddenly you can write a number into that x property and break your original contract.

Until the language provides a true readonly view of properties, then you need to try to work around it, with the above caveat that it's likely to be only somewhat useful. If your goal is to have a type like CustomReadonly such that T extends CustomReadonly but not CustomReadonly extends T, then you are saying you want CustomReadonly to produce a proper supertype of T. Conceptually that looks like type CustomReadonly = T | SomethingElse, and that SomethingElse cannot be assignable to T. But of course that will mean that when you view a CustomReadonly you won't be sure if it's a T unless you "test" it in some way. Maybe you want to make your type recursive, so type CustomReadonly = T extends object ? {[K in keyof T]: CustomReadonly} | SomethingElse : T. It depends on the use case and how far down into the object you want to recurse.

Okay, so here's one possible particular approach:

type CustomReadonly = T extends object ? {
  readonly [K in keyof T]: CustomReadonly | ReadonlyMarker
} : T
type ReadonlyMarker = { readonlyMarker: true }

function mutateTestThrough(t: CustomReadonly) { mutateTest(t); // error }

mutateTestThrough(new Test()); // okay

Here a CustomReadonly marks each property as readonly and unions it with ReadonlyMarker, a type I made up that probably won't exist in any real-world code. It's effectively a nominal "brand". Then CustomReadonly looks like { readonly id: number | ReadonlyMarker }. So you can't directly write to id because it's readonly, nor can you assign it to number because it's number | ReadonlyMarker. You might not like this last part. Maybe you want the property to be a number. But then something in the parent object needs a union like

type CustomReadonly = T extends object ? {
  readonly [K in keyof T]: CustomReadonly } | ReadonlyMarker
  : T

And then CustomReadonly will not even let you look at the id property until you assure TypeScript it's a real Test. So you'd have to work around that too, either with type assertions or useless runtime tests ("is "readonlyMarker" in obj?").

So there you go. You can do something, but it's not great.

Playground link to code