TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

TC39 Accessor Decorators unable to use `declare`

Open NullVoxPopuli opened this issue 10 months ago • 1 comments

🔎 Search Terms

decorator, declare, accessor

🕗 Version & Regression Information

afaict, this never worked? (it should tho)

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgLIE8DK0BuwkDeAvgNwBQZEAHgA4D2UYyCANnAM7vIBiddyBMsmQABdrnwpESTg2QATCKzhQUMPgC40WCUnJEKAekPJ2NJchMAVAMIBmAJzIAIkoZwwDMjACuIBGDAdCCmuhAAPABqcCw+EAB8ABRCyGAqAOYQYFo2bJwAgggy7AyuCO6eUFYZWeF+ANYgdADuIAA0yNGxCR0p5eDU2ci5HOyFxaVuUB4MNsGQVGBkAJQ5eWNFELJQZRUMAEpbPixgdSCNLe2dMXHx5GTGyMAAtjQsEM8Q4B5BIYlg6HMCjoWxAAHImM8PJAoB1qIgwCx0MtvH4Ar9QlA8EgojcEokAHREjLsLQNJqtADaAF1VshyZcBClVGAfFAQgAGZAcennCkhHldOL6ChAA

💻 Code

interface MyService{};

export class Foo {
  @service accessor declare foo: MyService;
}

// spec / TC39 Decorator
function service<Value>(
  target: ClassAccessorDecoratorTarget<unknown, Value>, 
  context: ClassAccessorDecoratorContext
): ClassAccessorDecoratorResult<unknown, Value>;

// implementation (type doesn't matter, exactly)
function service<Value>(...args: unknown[]): unknown {
  return 0 as unknown as Value;
}


🙁 Actual behavior

error here:

export class Foo {
   @service accessor declare foo: MyService;
// ^
// \- Decorators are not valid here (1206)
}

🙂 Expected behavior

no error - decorators can define values on access, lazily, so declare should be allowed since declare means "TS doesn't normally know this can have a value, but I'm declaring that 'something' that TS can't see is setting that value".

where as, what does "work" is ! -- which is often linted against, and colloquially means approx "idgaf, I'm right" -- which is an appropriate thing to lint against.

Additional information about the issue

No response

NullVoxPopuli avatar Mar 13 '25 20:03 NullVoxPopuli

This seems to be unrelated to accessors and applies to declared properties in general.

When implementing this feature it was intentional: #50820

The following items are not currently supported:
Decorators on a declare field.

I can't find any updated information.


since declare means "TS doesn't normally know this can have a value, but I'm declaring that 'something' that TS can't see is setting that value".

where as, what does "work" is ! -- which is often linted against, and colloquially means approx "idgaf, I'm right" -- which is an appropriate thing to lint against.

I wouldn't say this is entirely correct. With useDefineForClassFields enabled the declare variant will omit the property entirely. declare is generally used for things outside of TypeScripts realm (e.g. JavaScript libraries).

Using the non-null assertion operator generally means "trust me, this will be provided, even if you can't figure out how", which is your use-case. Common examples are decorators or init functions.

IMO the linter should either complain about both declare and ! or neither, because essentially the result is the same: A value should be provided, but isn't.

MartinJohns avatar Mar 14 '25 10:03 MartinJohns

I suppose this could be fixed if decorators can set the type of the field.

Since decorators can add an initializer, it kinda makes sense that a decorator could define the type for a decorated thing -- then both declare and ! wouldn't make sense 🎉

NullVoxPopuli avatar Mar 14 '25 20:03 NullVoxPopuli

This is intended behavior. declare fields are a transitional feature designed specifically to allow decorators on fields where "set" semantics are still required when switching to useDefineForClassFields: true (which is the default for ES2021 and later). accessor fields indicate a runtime transformation to a get/set pair backed by a private field, which is wholly incompatible with the behavior of declare in this context.

It should be noted that declare fields primarily exist so that legacy decorators could utilize Object.defineProperty to convert the field to an accessor pair. This is behavior that major runtimes were strongly opposed to as it results in deoptimizations and performance issues. Instead, accessor fields were added so that this transformation could be performed statically by the runtime. As a result, if you wanted to transition from TypeScript's legacy decorators to native ECMAScript decorators, you would want to change your declare field to an accessor field and change your decorator from one that installs a new accessor pair to one that receives the accessor pair generated by the runtime.

rbuckton avatar Mar 14 '25 22:03 rbuckton