Assigning Readonly<T> to T should be an error.
cefn opened this issue ยท 3 comments
Feature Request
It should be impossible to assign a Readonly<T>
value to a name having the mutable T
type, so that Readonly
can do its work of preventing later runtime errors when members are manipulated.
A Readonly is structurally different from a T in that the set
of every property is omitted, (even though it's really there) in the same way that push
is omitted from a ReadonlyArray even though it's really there.
This is already respected in Typescript when a property is known to be Readonly<T>
but this strictness is obliterated by allowing Readonly<T>
to be assigned to T
without compiler errors, which has the effect of re-publishing its shadow set
operations which should never be called.
I am sure this must have been raised and discussed somewhere, but I can't find it with inevitably broad search terms, sorry.
Assignment of a non-array Readonly<T>
should be a compile-time error in the same way that assignment of a ReadonlyArray<T>
to a T[]
type is a compiler error. This is far preferable to waiting for a runtime error when members are manipulated.
This would mean that e.g. the return type from Object.freeze
can do its work correctly of ensuring Immutability, but with editor and build-time support rather than waiting for production errors.
It will also mean that libraries aiming to pass around Immutable objects e.g. https://cefn.com/lauf/api/types/_lauf_store.Immutable.html can benefit from compile-time support consistent with the definition of Readonly without writeability leaking in through understandable implicit errors, which then bypass compile-time checks.
Those wishing to bypass the support can easily do so. Perhaps there could be a strictness flag associated?
Structural difference underpinning the type-incompatibility would look like this (example use case is further down the issue)...
class foo {
private _bar: boolean = false;
get bar(): boolean {
return this._bar;
}
set bar(value: boolean) {
this._bar = value;
}
}
class foo {
private _bar: boolean = false;
get bar(): boolean {
return this._bar;
}
// this should be structurally absent from a Readonly<foo>
// set bar(value: boolean) {
// this._bar = value;
//}
}
๐ Search Terms
Readonly mutable assignment
๐ Version & Regression Information
This has been an established behaviour for some time.
โฏ Playground Link
๐ป Code
Problem case where T is not an array.
// Problem case - Object
let planet = {
name: "earth",
satellites: {
moon: 70000000000000000000000,
},
};
const frozenPlanet = Object.freeze({
name: "earth",
satellites: {
moon: 70000000000000000000000,
},
});
// correctly a compile-time error (set is not valid)
frozenPlanet.name="mars";
// incorrectly allowed, (implicitly re-adding set) and leading to the uncaught error below
planet = frozenPlanet;
// raises a runtime error
planet.name = "mars"
Correctly-handled case where T is an array.
// correct case Array
let planets = [{
name: "earth",
satellites: {
moon: 70000000000000000000000,
},
}];
const frozenPlanets = Object.freeze([{
name: "earth",
satellites: {
moon: 70000000000000000000000,
},
}]);
// throws TypeError
frozenPlanets.push(planets[0]);
// assignment prevented - compile error
planets = frozenPlanets;
// would also throw a TypeError if you could reach this line
planets.push(frozenPlanets[0]);
๐ Actual behavior
The value was accepted as valid for planet
even though it was a Readonly<typeof planet>
, the subsequent treatment of planet as writeable when assigning to the name
property will create a runtime error that could be caught at compile time if the availability of set was considered a structural feature of the value's type.
๐ Expected behavior
I expected it to be impossible to assign Readonly to T as they are structurally incompatible with each other.
Duplicate of #13347.
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.