Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I change a readonly property in TypeScript?

Tags:

typescript

I want to be able to make readonly properties (not getters) for users of my class, but I need to update them internally; is there a way to do this and allow to be changed internally? (and make sure TypeScript blocks most attempts to change the value)

(In my case it's a game engine and getters/setters [functions in general] are a bad option)

like image 697
James Wilkins Avatar asked Oct 08 '17 18:10

James Wilkins


4 Answers

You could make use of the improved mapped type modifiers since Typescript 2.8.

For example, let's say that UI layer (and all others except persistence layer) shall only get a readonly version of your domain entity. Persistence layer is a special case, since it somehow must know how to copy all internals into the database. In order to do so, we don't want to make defensive copies everytime and just use the readonly typescript modifier for that purpose.

Your readonly entity would be:

class Immutable {
    constructor(
        public readonly myProp: string ) {}
}

The mutable type of your entity:

type Mutable = {
     -readonly [K in keyof Immutable]: Immutable[K] 
}

Note the special -readonly syntax to remove the flag (also works with optionals).

In one limited place (here the persistence layer) we can convert Immutable to Mutable by doing:

let imm = new Immutable("I'm save here")
imm.myProp = "nono doesnt work. and thats good" // error
let mut: Mutable = imm  // you could also "hard" cast here: imm as unknown as Mutable
mut.myProp = "there we go" // imm.myProp value is "there we go"

Hope that helps.

like image 107
ford04 Avatar answered Nov 17 '22 05:11

ford04


There are actually 3 ways I know of. If you have a class like this:

class GraphNode {
    readonly _parent: GraphNode;
    add(newNode: GraphNode) { /* ...etc... */ }
}
var node = new GraphNode();

In the add() function you could do either:

  1. newNode[<any>'_parent'] = this; - Works, but BAD IDEA. Refactoring will break this.

    Update: Seems newNode['_parent'] = this; will work just fine now without <any> in newer versions of TypeScript, but refactoring will still break it.

  2. (<{_parent: GraphNode}>newNode)._parent = this; - Better than 1 (not the best), and although refactoring breaks it, at least the compiler will tell you this time (since the type conversion will fail).
  3. BEST: Create an INTERNAL interface (used by yourself only):

    interface IGraphObjectInternal { _parent: GraphNode; }
    class GraphNode implements IGraphObjectInternal {
        readonly _parent: GraphNode;
        add(newNode: GraphNode) { /* ...etc... */ }
    }
    

    Now you can just do (<IGraphObjectInternal>newNode)._parent = this; and refactoring will also work. The only caveat is that if you export your class from a namespace (the only reason to use an internal interface IMO) you'll have to export the interface as well. For this reason, I sometimes will use #2 to completely lock down internals where there's only one place using it (and not advertise to the world), but usually #3 if I need to have many properties to work with referenced in many other locations (in case I need to refactor things).

You may notice I didn't talk about getters/setters. While it is possible to use only a getter and no setter, then update a private variable, TypeScript does not protect you! I can easily do object['_privateOrProtectedMember'] = whatever and it will work. It does not work for the readonly modifier (which was in the question). Using the readonly modifier better locks down my properties (as far as working within the TypeScript compiler is concerned), and because JavaScript doesn't have a readonly modifier, I can use various methods to update them with workarounds on the JavaScript side (i.e. at runtime). ;)

Warning: As I said, this only works within TypeScript. In JavaScript people can still modify your properties (unless you use getters only with non-exposed properties).

Update

Since typescript 2.8 you can now remove the readonly modifiers:

type Writeable<T> = { -readonly [P in keyof T]: T[P] };

and also the optional modifier:

type Writeable<T> = { -readonly [P in keyof T]-?: T[P] };

More here: Improved control over mapped type modifiers

like image 40
James Wilkins Avatar answered Nov 17 '22 06:11

James Wilkins


My current solution for TypeScript 3.6.3

type Mutable<T> = {
   -readonly [k in keyof T]: T[k];
};

class Item {
  readonly id: string;

  changeId(newId: string) {
    const mutableThis = this as Mutable<Item>;
    mutableThis.id = newId;
  }
}
like image 25
Reinhard Avatar answered Nov 17 '22 05:11

Reinhard


The answer posted by OP here is the best answer, not this one. Which is just to use an interface and not export it.

interface IGraphObjectInternal { _parent: GraphNode; }
export class GraphNode implements IGraphObjectInternal {
  // tslint:disable-next-line:variable-name
  // tslint:disable-next-line:member-access
  // tslint:disable-next-line:variable-name
  public readonly _parent: GraphNode;
  public add(newNode: GraphNode) {
    (newNode as IGraphObjectInternal)._parent = this;
  }
}

I tried this earlier and had some problem (not sure why, but tried again now and it works just fine.

Leaving the answer here just for the fun of playing with it.

TypeScript provides readonly keyword which allows setting value on initialization or in constructor only.

If you want to change the value any time, then what you need is a read-only property, which is known as a "get" property.

Example:

class MyClass { 
  private _myValue: WhateverTypeYouWant;

  get myValue() {
    return this._myValue;
  }

  doSomething(inputValue: WhateverTypeYouWant) {
    // Do checks or anything you want.
    this._myValue = inputValue; // Or anything else you decide
  }
}

It's worth mentioning that users may still be able to call myObject['_myValue'] and access the property. TypeScript will not tell them about it in intellisense though, and if they do use your code this way, they are using your library in an unsupported way and shooting themselves in the foot (note that this is client-side code anyway, so the code is available to read).

Check the official documentation on how this works.


Update

If you really want to use readonly and force it to work, you can do it like this:

class Test {
    readonly value = "not changed";

    changeValue() { 
        this["value" as any] = "change from inside";
    }
}

But as I mentioned in my comment on this answer, and I show in the runnable version of this example, the semantics are the same in the sense that both private and readonly can be changed from outside if the users really want to.


Update 2

In further comments you bring an interesting scenario, game development, where function call is considered expensive. I cannot validate how expensive property access might be (Which is the recommended path for this generally), but here's the answer I think you are looking for:

If you really really want to do set the readonly member, and just want to make sure you have refactoring support, change this["value" as any] = to (this.value as Test['value']) = (where Test here is the class name, and value is the property name).

class Test {
    // We had to set the type explicitly for this to work
    // Because due to initial `= "not changed"`
    //  `value` property has type `"not changed"` not `string`
    readonly value: string = "not changed";

    changeValue() { 
        (this.value as Test['value']) = "change from inside";
        alert(this.value);
    }
}

const test = new Test();

test.changeValue();

(test.value as Test['value']) = 'change from outside';
alert(test.value);

Runnable Example


Update 3

Although the syntax (this.value as Test['value']) = works in official TypeScript playground, as proven by the link at the end of Update 2 in this answer, it doesn't work in VS Code (and maybe other TS environments).

You need to change it to this['value' as Test['value']] = (where, again, Test is a class name and value is a property name).

The working code becomes:

class Test {
  // We had to set the type explicitly for this to work
  // Because due to initial `= "not changed"`
  //  `value` property has type `"not changed"` not `string`
  readonly value: string = "not changed";

  changeValue() {
    this['value' as Test['value']] = "change from inside";
    alert(this.value);
  }
}

const test = new Test();

test.changeValue();

test['value' as Test['value']] = 'change from outside';
alert(test.value);

Runnable Example

Limited Refactoring

Since refactoring is the reason for asking the question I have to mention that besides being ugly, the workaround here offers only limited refactoring support.

That means, if you misspell property name (value in the sample) in any part of the assignment this['value' as Test['value']] = ..., TypeScript will give you a compile time error.

The problem though is, at least in VS Code in my quick test, when you rename the property (from value in the sample to anything else), TypeScript / VS Code doesn't update the references to it that are implemented using this workaround.

It still gives you a compile time error, which is better than leaving invalid code without errors, but you'd want it to rename the property for you too.

Luckily having to do this with a string replace (of ['value' as Test['value']] in the sample) seems to be generally safe from false matches, but still, it's silly, and less than desired, but I think this is as far as this gets.

like image 6
Meligy Avatar answered Nov 17 '22 05:11

Meligy