Readonly Modifier

Hello,

I've mentioned this in another proposal, but I think it's worth having one of its own.

I am developing an object-oriented library in which I need to prevent the user from directly modifying the internal state.

This is how I do it with Javascript...

class Store {
  #state = 0;

  get state() {
    return this.#state;
  }
}

This is a bit verbose and hard to understand, plus whenever I use state inside of Store, I need to prefix it with #. I'd love to use the readonly modifier in Typescript here.

Then it could be...

class Store {
  readonly state = 0;
}

We could use constructor properties too! I'm sure @jithujoshyjy would be interested :)

class Store {
  constructor(readonly state) {};
}

Thank you :grinning:.

Doesn't TypeScript's readonly modifier also prevent you from modifying that property from within the class too? Is this what you're looking for, or are you looking for a way to be able to internally modify a property, but externally its readonly (which is what your initial example gives you)?

1 Like

Oops, I tested that with this code earlier and it seemed to work.

class A {
  readonly property;

  constructor(property) {
    this.property = property;
  }
}

I didn't know readonly properties were not that readonly inside the constructor of a class. It still throws if I try to change it inside another method.

If I initially do assign it to a value, it doesn't let me assign it to anything except that value, weird.

My bad sorry :pensive:.

I don't know much about the decorator proposal, but it sounds like something that may be solvable with a decorator? Maybe? Hopefully? Someone who's smart out there can let me know.

I suppose you probably need some way of detecting where the user is accessing the property from (weather its from within the class or outside), and I guess that's not currently information that's getting exposed.

1 Like

afaik a decorator can only alter the thing it's decorating. Meaning a decorator on a property cannot add more properties to the class. And you need two properties for this, there is no way a single property could be readonly from the outside, while at the same time writable from the inside.

What else one could do is come up with shortcuts to make the getter less verbose, like

get state = #state;
1 Like

Actually, I think it is almost possible. After doing a quick glance over the decorator proposal, I see they're providing a new "accessor" property type, that auto-defines a getter and setter on the class, and can be decorated.

So, you could have a decorator such as this:

class MyClass {
  @externallyReadOnly accessor myData = 2
}

And have the decorator desurgar that to this:

class MyClass {
  get myData() {
    return ...
  }
  set myData(newVal) {
    if (!new.self) throw new Error('Can not read this property from outside')
    ... = newVal
  }
}

The ... would be replaced with accessing/setting metadata stuff, that I see decorators support. The decorator would be able to properly default-initialize to the correct value. (In the above example, it would be default-initialized to 2).

The only missing piece is "new.self" that @Jonas_Wilms proposed here in order to enable private constructors. The idea of "new.self" is that it'll simply return true if you're being called from within a class definition, and false otherwise. I don't think this exact idea would be useable here, as the actual getter/setter definitions are created from outside the class, by the decorator, so new.self wouldn't be able to target the class, but perhaps we could find a solution that could work for both use cases.

I'd rather we find a way to empower decorators to support this kind of feature (and perhaps provide built-in convenient decorators if wanted), then add new syntax to achieve this.

Edit: But the get state = #state syntax shorthand looks pretty cool too,. Or, even better, if you could just define getters as arrow functions, so they weren't so verbose, e.g. get state = () => this.#state

1 Like

readonly / immutable structures are a thing that I would like to see in the language. But there's this battle between visibility & mutability in OO languages. While there's a plethora of methods to control mutability in JS like Object.(preventExtensions, freeze, seal), getters & setters... None of them seem as ergonomic as private class fields (that favours visibility).
I expected that syntax for readonly class fields but...
My intent came from the Records and Tuples proposal:

[1, 2, 3] // mutable - Array
#[1, 2, 3] // immutable - Tuple

{ a: 1, b: 2 } // mutable - Object
#{ a: 1, b: 2 } // immutable - Record

// likewise
class {
    mutable_prop
    #immutable_prop
}

// maybe i wish we could have an immutable class like
class name #{
}
// maybe that's utter nonsense!

Anyhow here we are... got to accept what we have.
I guess someone will surely argue that a getter/setter(s) can act as desugared readonly modifier.

I'm not sure about denoting a parameter property using the readonly modifier.

1 Like

That syntax is already reserved as Javascript private class fields. Plus there is Object.freeze() for immutable data structures.

are you sure you've read what I said above??:wink:
skipping ahead is not a good practice you know​:grin::grin:

1 Like

This is possible with the latest version of the decorators proposal. But the class also needs to be decorated because the field decorator on its own can only modify the field it's own, but it can delegate adding the public getter to the class decorator.

const MAKE_PUBLIC = Symbol();

function readonly(_, {name, access: { get }, setMetadata }) {
  const publicName = name.replace(/^#/, '');
  setMetadata(MAKE_PUBLIC, { publicName, get })
}

function enableReadonly(klass, context) {
  const readonlyFields = klass.prototype[Symbol.metadata][MAKE_PUBLIC]['private'];
  for (const {publicName, get} of readonlyFields) {
    Object.defineProperty(klass.prototype, publicName, { get })
  }
}

Used like this:

@enableReadonly
class C {

  @readonly
  #count = 1;

  increment() {
    this.#count += 1;
  }
  
}

const c = new C;
console.log(c.count)

playground: Javascript Decorators

3 Likes

This can also be done with a single init decorator.

function readonly(_, {name, access: { get }, addInitializer }) {
  const publicName = name.replace(/^#/, '');
  addInitializer(function () {
    Object.defineProperty(this, publicName, { get })
  })
}

class C {
  @init:readonly
  #count = 1;
}

const c = new C;
console.log(c.count) // 1
c.count = 2 // Error

Example in playground.

3 Likes

Nice!

Seems that the micro tradeoff between the two approaches is that having that extra initialiser run on each instance could get expensive iff it was in a program hot path. So having the paired class decorator could be seen as a micro-benchmark performance gain.