microsoft/TypeScript

Assigning Readonly<T> to T should be an error.

cefn opened this issue ยท 3 comments

cefn commented

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

Object problem case

Array correct case

๐Ÿ’ป 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.

cefn commented

Closed in favour of #13347 thanks!

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.