Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow readonly with accessor #55289

Open
5 tasks done
jrandolf opened this issue Aug 7, 2023 · 5 comments
Open
5 tasks done

Allow readonly with accessor #55289

jrandolf opened this issue Aug 7, 2023 · 5 comments
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript

Comments

@jrandolf
Copy link

jrandolf commented Aug 7, 2023

πŸ” Search Terms

accessor
decorator
readonly

βœ… Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

Add readonly as a modifier for accessor fields.

πŸ“ƒ Motivating Example

Suppose you have an injection framework with @provides, @consumes, and @inject. Consider the example

class A {
  @provides(Number)
  readonly value: number;

  @inject([Number])
  accessor b = new B();

  constructor(value: number) {
    this.value = value;
  }
}

class B {
  @consumes(Number)
  readonly value: number;
}

Note because @provides is a field decorator, it cannot know when value is assigned unless it's in the initializer. As a consequence, b will not have value injected since @provides doesn't know when to reinject. This is fixed if you do

class A {
  @provides(Number)
  readonly accessor value: number;

  @inject([Number])
  accessor b = new B();

  constructor(value: number) {
    this.value = value;
  }
}

class B {
  @consumes(Number)
  readonly value: number;
}

πŸ’» Use Cases

  1. What do you want to use this for?
    Accessor declarations that shouldn't be modified after being used in the constructor.
  2. What shortcomings exist with current approaches?
    You can only mark a field with @readonly using JSDoc and just hope that other developers don't touch the field.
  3. What workarounds are you using in the meantime?
    @readonly using JSDoc
@MartinJohns
Copy link
Contributor

MartinJohns commented Aug 7, 2023

This seems intentional: #51820

And in the implementing PR it's explicitly mentioned: #49705

accessor cannot be used with readonly or declare on the same field declaration.

See also this comment: #49705 (comment)

@jrandolf
Copy link
Author

jrandolf commented Aug 7, 2023

I see. @rbuckton FWIU from #49705 (comment), the blocker seems to be that accessors also create a setter? I think this is expected. In fact, it's necessary that the accessor creates the setter even in a readonly situation.

The reason is readonly-members in TypeScript are not readonly when initialized (obviously) or assigned in the constructor. Since accessors do not distinguish assignment in constructor with assignment elsewhere, readonly setting becomes important. In particular, it's possible to set up event listeners in a constructor that continually modify the member, yet we still want other methods to only access it.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Revisit An issue worth coming back to labels Aug 7, 2023
@jun-sheaf
Copy link

@RyanCavanaugh @rbuckton Any updates here?

@rbuckton
Copy link
Member

rbuckton commented Mar 22, 2024

I believe this would directly conflict with the Grouped and Auto Accessors Proposal, and I would rather not have two syntaxes for this.

For the time being, are you able to use a normal getter? If so, this might be an option:

class A {
  #value: number;

  @provides(Number)
  get value() { return this.#value; }

  @inject([Number])
  accessor b = new B();

  constructor(value: number) {
    this.value = value;
  }
}

If not, and you are using native decorators, then you could work around this through indirection:

class A {
  @provides(Number)
  #value: number;
  
  get value() { return this.#value; }

  @inject([Number])
  accessor b = new B();

  constructor(value: number) {
    this.#value = value;
  }
}

If you are using our legacy --experimentalDecorators option, then you could work around it in this fashion:

class A {
  @provides(Number)
  private _value: number;
  
  get value() { return this._value; }
  
  @inject([Number])
  accessor b = new B();

  constructor(value: number) {
    this._value = value;
  }
}

I should also note that in the Grouped and Auto-Accessors proposal, this would be accomplished as follows (though the syntax is subject to change at this early stage):

class A {
  @provides(Number)
  accessor value: number { get; #set; }

  @inject([Number])
  accessor b = new B();

  constructor(value: number) {
    this.#value = value;
  }
}

@jun-sheaf
Copy link

jun-sheaf commented Mar 22, 2024

I'm afraid the proposed solutions do not work. We are using native decorators and require the field to be an accessor. Using a normal getter is possible, but for every instance is a lot of boilerplate. I guess I'll put an issue up about this in ts-eslint.

[EDIT: NVM, I just learned that https://www.npmjs.com/package/eslint-config-standard-with-typescriptwas using an outdated version of ts-eslint...their replacement is also out-dated sadly.]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants