简体   繁体   English

如何更改 TypeScript 中的只读属性?

[英]How can I change a readonly property in TypeScript?

I want to be able to make readonly properties (not getters) for users of my class, but I need to update them internally;我希望能够为我的班级用户创建readonly属性(而不是 getter),但我需要在内部更新它们; is there a way to do this and allow to be changed internally?有没有办法做到这一点并允许在内部进行更改? (and make sure TypeScript blocks most attempts to change the value) (并确保 TypeScript 阻止大多数更改值的尝试)

(In my case it's a game engine and getters/setters [functions in general] are a bad option) (就我而言,它是一个游戏引擎,而 getter/setter [一般功能] 是一个糟糕的选择)

You could make use of the improved mapped type modifiers since Typescript 2.8.您可以使用自 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.例如,假设 UI 层(以及除持久层之外的所有其他层)只能获得域实体的readonly版本。 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.为了做到这一点,我们不想每次都制作防御性副本,只需为此目的使用readonly typescript 修饰符。

Your readonly entity would be:您的readonly实体将是:

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).请注意删除标志的特殊-readonly语法(也适用于选项)。

In one limited place (here the persistence layer) we can convert Immutable to Mutable by doing:在一个有限的地方(这里是持久层),我们可以通过执行以下操作将Immutable转换为Mutable

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.希望有帮助。

There are actually 3 ways I know of.我知道实际上有 3 种方法。 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:add()函数中,您可以执行以下任一操作:

  1. newNode[<any>'_parent'] = this; - Works, but BAD IDEA. - 有效,但坏主意。 Refactoring will break this.重构将打破这一点。

    Update: Seems newNode['_parent'] = this;更新:似乎newNode['_parent'] = this; will work just fine now without <any> in newer versions of TypeScript, but refactoring will still break it.在较新版本的 TypeScript 中没有<any>可以正常工作,但重构仍然会破坏它。

  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). - 比 1 好(不是最好的),虽然重构破坏了它,但至少编译器这次会告诉你(因为类型转换会失败)。
  3. BEST: Create an INTERNAL interface (used by yourself only): BEST:创建一个INTERNAL接口(仅供您自己使用):

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

    Now you can just do (<IGraphObjectInternal>newNode)._parent = this;现在你可以做(<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.唯一需要注意的是,如果您从命名空间导出您的类(使用内部接口 IMO 的唯一原因),您还必须导出该接口。 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).出于这个原因,我有时会使用 #2 来完全锁定只有一个地方使用它的内部结构(而不​​是向全世界宣传),但通常 #3 如果我需要在许多其他位置引用许多属性来使用(以防我需要重构事物)。

You may notice I didn't talk about getters/setters.你可能注意到我没有谈论 getter/setter。 While it is possible to use only a getter and no setter, then update a private variable, TypeScript does not protect you!虽然可以只使用 getter 而不使用 setter,然后更新私有变量,但 TypeScript 并不能保护您! I can easily do object['_privateOrProtectedMember'] = whatever and it will work.我可以很容易地做object['_privateOrProtectedMember'] = whatever ,它会起作用。 It does not work for the readonly modifier (which was in the question).它不适用于readonly修饰符(这是在问题中)。 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 (ie at runtime).使用readonly修饰符可以更好地锁定我的属性(就在 TypeScript 编译器中工作而言),并且因为 JavaScript 没有readonly修饰符,我可以使用各种方法通过 JavaScript 端的变通方法更新它们(即在运行时)。 ;) ;)

Warning: As I said, this only works within TypeScript.警告:正如我所说,这只适用于 TypeScript。 In JavaScript people can still modify your properties (unless you use getters only with non-exposed properties).在 JavaScript 中,人们仍然可以修改您的属性(除非您仅使用具有非公开属性的 getter)。

Update更新

Since typescript 2.8 you can now remove the readonly modifiers:从 typescript 2.8 开始,您现在可以删除 readonly 修饰符:

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更多信息: 改进对映射类型修饰符的控制

The answer posted by OP here is the best answer, not this one. OP在这里发布的答案是最佳答案,而不是这个答案。 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. TypeScript 提供了readonly关键字,它只允许在初始化或构造函数中设置值。

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.如果您想随时更改该值,那么您需要的是一个只读属性,它被称为“get”属性。

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.值得一提的是,用户仍然可以调用myObject['_myValue']并访问该属性。 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).但是,TypeScript 不会在智能感知中告诉他们,如果他们确实以这种方式使用您的代码,那么他们正在以不受支持的方式使用您的库,并在脚下射击(请注意,无论如何这是客户端代码,因此代码可供阅读)。

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:如果你真的想使用readonly并强制它工作,你可以这样做:

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.但正如我在对此答案的评论中提到的,并且我在本示例可运行版本中显示,语义是相同的,如果用户真的想要,可以从外部更改privatereadonly


Update 2更新 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).如果您真的真的想设置readonly成员,并且只想确保您有重构支持,请将this["value" as any] =更改为(this.value as Test['value']) = (where Test这里是类名, value是属性名)。

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更新 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).尽管语法(this.value as Test['value']) =在官方 TypeScript 操场上有效,正如本答案中更新 2 末尾的链接所证明的那样,但它在 VS Code(可能还有其他 TS环境)。

You need to change it to this['value' as Test['value']] = (where, again, Test is a class name and value is a property name).您需要将其更改为this['value' as Test['value']] = (同样, Test是一个类名, value是一个属性名)。

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.这意味着,如果您在赋值的任何部分拼错了属性名称(示例中的valuethis['value' as Test['value']] = ... ,TypeScript 会给您一个编译时错误。

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.这个问题虽然是,至少在VS代码在我的快速测试,当你(从命名属性value的样品中的其他东西),打字稿/ VS代码不会更新使用此解决办法来实现对它的引用。

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.幸运的是,必须使用字符串替换(示例中的['value' as Test['value']] )来执行此操作似乎通常可以避免错误匹配,但仍然很愚蠢,而且不太理想,但我认为到此为止。

My current solution for TypeScript 3.6.3我当前的 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;
  }
}

Here's the solution I like best (using a tip courtesy of James Wilkins).这是我最喜欢的解决方案(使用 James Wilkins 提供的提示)。 My thought is that write access should be only allowed within the class, hence making the getter private.我的想法是写访问应该只允许在类中,因此将 getter 设为私有。

type Writable<T> = { -readonly [K in keyof T]: T[K] }; 

class TestModel {
  readonly val: number;

  setVal(value: number) {
    this.asWriteable.val = value;
  }

  // use this.asWriteable.* to write to readonly fields
  private get asWriteable(): Writable<TestModel> {
    return this as Writable<TestModel>;
  }
}

Solution 1解决方案1

Use the readonly keyword.使用readonly关键字。

It instructs the TypeScript compiler that the member must be set immediately, or within the constructor.它指示 TypeScript 编译器必须立即设置该成员,或在构造函数中设置该成员。 It has the same syntactic semantics as readonly in C#.它与 C# 中的readonly具有相同的句法语义。

Note - readonly is not respected at the JavaScript level;注意 - 在 JavaScript 级别不考虑readonly it's purely a compiler check!这纯粹是一个编译器检查!

Solution 2解决方案2

Use a get-only accessor property使用仅获取访问器属性

Object.definedProperty(yourObject, "yourPropertyName", {
    get: function() { return yourValue; }
    enumerable: true,
    configurable: false
});

Solution 3解决方案3

Use a read-only value property使用只读值属性

Object.defineProperty(yourObject, "yourPropertyName", {
    value: yourValue,
    writable: false
});

Solution 4解决方案4

Use Object.freeze to prevent changes to your object使用Object.freeze防止更改您的对象

class Foo {
    constructor(public readonly name: string) {
        Object.freeze(this);
    }
}

Note, the code above ensures that name is readonly in TypeScript, and cannot be mutated in JavaScript because the object is frozen.注意,上面的代码确保 name 在 TypeScript 中是readonly的,并且不能在 JavaScript 中改变,因为对象被冻结了。

Solution 5解决方案5

Use const使用const

const x = 123;
x = 456 // Error! const declarations cannot be reassigned.

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM