Is there an elegant way to set a class property only once?

1.7k views Asked by At

In TypeScript, while in a class, is there an elegant way to set a value only once? In other words, is there an equivalent of setting a value as readonly after giving it a value?

For example:

class FooExample {
    public fixedValue: string;

    public setFixedValue(value: string): void {
        if (!this.fixedValue) {
            // this value should be immutable
            this.fixedValue = value;
        }
    }
}

I am not looking for a getter, because the class property could be changed within the class itself.

2

There are 2 answers

0
ebakunin On BEST ANSWER

After reading the suggested answers and doing my own research I believe I have found the most elegant solution. You first define the value as an Object and then freeze() the Object after populating the related property.

class FooExample {
    public fixedValue = {
        value: '';
    };

    public setFixedValue(value: string): void {
        if (!Object.isFrozen(this.fixedValue)) {
            this.fixedValue.value = value;
            Object.freeze(this.fixedValue);
        }
    }
}
0
Oleg Valter is with Ukraine On

You could use a "smart" setter to achieve this. The idea is to define a setter on the class, delete the setter from within itself and define a read-only property via the Object.defineProperty method called with the descriptor whose writable property is set to false.

The "changed within the class itself" part could be achieved via another property redefinition. Don't forget to make the property configurable in the first place.

Here is how the full thing could look like (I do not think there is a more elegant way to do this) - runtime version of the answer is not TypeScript specific:

class ReadonlyFields {
  set fixed(value) {
    Object.defineProperty(this, "fixed", {
      value,
      writable: false,
      configurable: true,
    });
  }

  doSomething() {
    Object.defineProperty(this, "fixed", {
      value: 24, //exact logic for changing the value is up to you
      writable: false
    });
  }
}

const r = new ReadonlyFields();
r.fixed = 42;
console.log(r.fixed); //42;
r.fixed = 24;
console.log(r.fixed); //42;
r.doSomething();
console.log(r.fixed); //24;

If you also want to enforce this from the type system standpoint, just make the property readonly:

class ReadonlyFields {
  readonly fixed!: number;

  setFixed(value: number) {
    Object.defineProperty(this, "fixed", {
      value,
      writable: false,
      configurable: true,
    });
  }

  doSomething() {
    Object.defineProperty(this, "fixed", {
      value: 24, //exact logic for changing the value is up to you
      writable: false,
    });
  }
}

const r = new ReadonlyFields();
r.setFixed(42); //OK
//@ts-expect-error
r.fixed = 24;
r.doSomething(); //OK