Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$Frozen<Type> object type #8842

Open
ibukanov opened this issue Feb 14, 2022 · 0 comments
Open

$Frozen<Type> object type #8842

ibukanov opened this issue Feb 14, 2022 · 0 comments

Comments

@ibukanov
Copy link

Proposal

$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 };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant