You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
$ReadOnly<T> utility type creates a read-only view of T. As such it does not model properly the effect of Object.freeze() as a reference to T can be assigned to $ReadOnly<T> allowing to change the view via modifying the reference of the original type. To address that it will be nice to have $Frozen<T> utility type. A mutable T or $ReadOnly<T> cannot be assigned to $Frozen<T> while $Frozen<T> can be assigned to $ReadOnly<T>. The only way to create instances of $Frozen<T> is to use Object.freeze().
This will allow to catch a missing call to Object.freeze().
Use case
Consider the following code:
type Foo = {| field: string |};
// frozen is assumed to be immutable with no way to change it
type WithFrozenFoo = {| +frozen: $ReadOnly<Foo> |};
let foo: Foo = { field: "test" };
foo.field = "new value";
let bar: WithFrozenFoo = { frozen: foo };
foo.field = "changed";
It presently type-checks and does not detect a missing call to Object.freeze() while the intended version should be:
let foo: Foo = { field: "test" };
foo.field = "new value";
let bar: WithFrozenFoo = { frozen: Object.freeze(foo) };
foo = { ...foo, field: "changed"};
Moreover, the bug is not detected by JS runtime at the point of foo.field assignment leading to rather hard to debug issues. This is especially actual in the code that uses immer-style libraries with the notion of draft objects that should be frozen when stored in persistent data structures.
It is possible to workaround the issue using a wrapper type like:
type FrozenGetter<T: {...}> = () => $ReadOnly<T>;
function frozenReference<T: {...}>(t: T): FrozenGetter<T> {
const readOnly = Object.freeze(t);
return () => readOnly;
}
type Foo = {| field: string| };
type WithFrozenFoo = {| +frozen: FrozenGetter<Foo> |};
let foo: Foo = { field: "test" };
foo.field = "new value";
let bar: WithFrozenFoo = { frozen: frozenReference(foo) };
But this requires to create an extra objects at runtime witch brings an overhead over the original solution. Another possibility is to create a synthetic type for the mutable object that cannot be assigned to Foo like in:
type Foo = {| +field: string |};
type MutableFoo = {|field: string, _mutableMarker?: void|};
function toFrozenFoo(foo: MutableFoo): Foo {
if (foo.hasOwnProperty("_mutableMarker"))
throw new TypeError("_mutableMarker should never be assigned");
return Object.freeze((foo: any));
}
type WithFrozenFoo = {| +frozen: Foo |};
let foo: MutableFoo = { field: "test" };
foo.field = "new value";
let bar: WithFrozenFoo = { frozen: toFrozenFoo(foo) };
// This generates an error.
let bar2: WithFrozenFoo = { frozen: foo };
While this does not impose any runtime overhead, it still requires an ugly hack with a never assigned field and the any type and the error message is not ideal:
17: let bar2: WithFrozenFoo = { frozen: foo };
^ Cannot assign object literal to `bar2` because property `_mutableMarker` is missing in `Foo` [1] but exists in `MutableFoo` [2] in property `frozen`. [prop-missing]
References:
10: type WithFrozenFoo = { +frozen: Foo };
^ [1]
12: let foo: MutableFoo = { field: "test" };
^ [2]
In addition this hack does not work with arrays.
With $Frozen<T> the code becomes:
type Foo = {| field: string |};
type WithFrozenFoo = {| +frozen: $Frozen<Foo> |};
let foo: Foo = { field: "test" };
foo.field = "new value";
// Generate an error as Foo is not compatible with `$Frozen<Foo>`
let bar: WithFrozenFoo = { frozen: foo };
The text was updated successfully, but these errors were encountered:
Proposal
$ReadOnly<T>
utility type creates a read-only view of T. As such it does not model properly the effect ofObject.freeze()
as a reference to T can be assigned to$ReadOnly<T>
allowing to change the view via modifying the reference of the original type. To address that it will be nice to have$Frozen<T>
utility type. A mutable T or$ReadOnly<T>
cannot be assigned to$Frozen<T>
while$Frozen<T>
can be assigned to$ReadOnly<T>
. The only way to create instances of$Frozen<T>
is to useObject.freeze()
.This will allow to catch a missing call to
Object.freeze()
.Use case
Consider the following code:
It presently type-checks and does not detect a missing call to
Object.freeze()
while the intended version should be:Moreover, the bug is not detected by JS runtime at the point of foo.field assignment leading to rather hard to debug issues. This is especially actual in the code that uses immer-style libraries with the notion of draft objects that should be frozen when stored in persistent data structures.
It is possible to workaround the issue using a wrapper type like:
But this requires to create an extra objects at runtime witch brings an overhead over the original solution. Another possibility is to create a synthetic type for the mutable object that cannot be assigned to Foo like in:
While this does not impose any runtime overhead, it still requires an ugly hack with a never assigned field and the any type and the error message is not ideal:
In addition this hack does not work with arrays.
With
$Frozen<T>
the code becomes:The text was updated successfully, but these errors were encountered: