Closed hax closed 1 year ago
My preference (and the current TypeScript implementation) is before export
. More complex decorators like Angular's Component decorator often span multiple lines and the export
keyword would end up oddly orphaned at the top otherwise.
export
@dec1
@dec2({
option1: "foo"
})
class C {
}
// vs
@dec1
@dec2({
option1: "foo"
})
export class C {
}
I suppose "both" might be an acceptable mix though:
@dec1
@dec2({
option1: "foo"
})
export @sealed class C {
}
The current proposal only permits decorators after export
. The DecoratorList is part of the ClassDeclaration, which is the thing that the export
keyword. Should we revisit this decision?
This might be a change vs previous versions, which should be documented as part of https://github.com/tc39/proposal-decorators/issues/50 .
I recall discussions with @wycats over the past two years where we discussed this and I had thought we had settled on "before export
" in the past.
One of the other reasons for TypeScript's implementation is that we have other possible keywords that can come before class
(i.e. declare
and abstract
) and it seemed weird to write:
export
@decorator1
@decorator2
abstract class C {
}
With multi-line decorators you can end up losing track of the export
keyword when looking at the class. It also fairly common to want to refactor your code by adding the export keyword and you might naturally assume you just put the keyword before class
(which is the case in other languages like Java (annotations) and C# (attributes)).
The only other part of the conversation I recall was whether we wanted to eventually allow users to decorate an export
binding separate from the class itself. Whether we would ever do such a thing and what that would mean is unclear.
I had suggested an extension to the syntax to support this in a way similar to how C# attributes allow you to specify placement, via a prefix:
[assembly: AssemblyVersion("1.0.0")]
class C {
[return: MarshalAs(UnmanagedType.LPWStr)]
string Method() { ... }
}
We could do the same thing with decorators:
@decorator1
@decorator2
@export: decorator3 // decorates the export binding
export class C {
@route("/foo")
@method: permit("administrators") // decorates the method (optional here)
@return: serializeAs("json") // decorates the return value
method() { ... }
}
I'm not necessarily saying we have to figure out what those mean, but that there's room in the design space to put together something meaningful that isn't dependent on whether the decorator is before or after the export
keyword.
export
is static, so I think decorating it is impossible. I think that's better to have the class's decorators just before the class
keyword because it clearly shows that the decorators apply to the class, and not to export
.
Given that export
isn't the declaration nor the thing being decorated, I think "after export" is the only thing that makes sense.
In other words, you don't need to "keep track" of export
when you're looking at a long chain of decorators, because the fact that it's exported isn't directly relevant to what's being decorated and how.
What about if a future proposal adds other keywords before class, for example abstract
, final
, sealed
, static
, etc.? Those are logically associated with the class
and not the ExportDeclaration, so if they are added there will be an odd mix of export
before and these after.
When TypeScript adds support for ES decorators it will be an all-or-nothing switch, so we can definitely support export
-before. Since TypeScript has export
-after currently, I will see if I can get some feedback from our community as to whether they find our current syntax ergonomic or unergonomic. Having used it myself for a few years now, I personally find export
-after the most ergonomic, easiest to refactor, and visually appealing.
That could be :
export @mydecorator abstract class {}
But I think abstract
, final
, sealed
, static
and etc. will probably be decorators, so :
export @mydecorator @abstract class {}
What about if a future proposal adds other keywords before class, for example abstract, final, sealed, static, etc.? Those are logically associated with the class and not the ExportDeclaration, so if they are added there will be an odd mix of export before and these after.
Why would these keywords make sense before export
rather than after? Would @ljharb 's logic in https://github.com/tc39/proposal-decorators/issues/69#issuecomment-370246996 apply differently to decorators vs these keywords?
I'm not talking about those keywords before export
, but rather the inconsistency of, say:
export
@dec
abstract class C {}
// vs
@dec
export abstract class C {}
In discussing this with others on the TypeScript team, so far the consensus is that we haven't yet heard any negative feedback about our current placement of "before export
", which generally indicates that our customers have been happy or at least accepting of that behavior after having decorators as a feature for several years.
Also, there are some things that can't currently be done in a decorator (i.e. final
or sealed
) so we can't necessarily wash our hands of the possibility of other future keywords.
I guess i don’t understand the inconsistency. If you add or delete the export keyword, the declaration should be the same, conceptually - id find it very inconsistent if the ordering wasn’t “export”, “decorators for the thing”, “thing I’m exporting”.
Coming from C#, I find the separation of the export
keyword from the class
keyword by its decorators confusing. In C#, you would write this:
[attr1]
[attr2]
public abstract class C {
}
Where public
is what makes the class reachable outside of the assembly (i.e. exported). If export
needs to be before decorators, it seems like a refactoring pitfall:
class B {
}
@dec1
@dec2
class C {
}
If I want to export B
, I add export
before the class
keyword. If I want to export C
its easy to assume that export
goes before the class
keyword, just like async
goes before the function
keyword. It all depends on how you view export
. The spec treats it as a declaration in its own right, but its fairly easy for someone to assume export
is just a modifier for class
just like static
is a modifier for a method:
class C {
@dec1
@dec2
method() {}
}
I would refactor method
by adding static
after the decorators, but before the method name.
I'd be happy if the spec allowed me to chose one or the other as a personal preference, something like (simplified for brevity's sake):
ExportDeclaration:
DecoratorList `export` ClassDeclaration[~Decoratable]
`export` Declaration[+Decoratable]
ClassDecoratorList[Decoratable]:
[empty]
[+Decoratable] DecoratorList
ClassDeclaration[Decoratable]:
ClassDecoratorList[?Decoratable] `class` BindingIdentifier ...
Something like that would allow you to choose either export @dec class C {}
or @dec export class C {}
, but would disallow @dec export @dec class C {}
.
@rbuckton The question is : What do you want to decorate with @decorator
(while reading from Left-To-Right) ?
// Decorate "export" ?
@decorator export abstract class A {}
// Decorate the abstract class ?
export @decorator abstract class A {}
// Decorate a method ?
export abstract class A {
@decorator
method() {}
}
OK, if TS has decorators before export, we are talking about a fairly large compatibility risk (where this proposal attempts to not require much changes in the decorator usage side, even if we are fine with requiring the definitions to change). For mitigations, we could
Does anyone know what syntax Babel uses here?
As I understand Babel have to follow TS because it's the goal of Babel 7 to compile TS to JS.
As I send earlier, using ES decorators will be a binary choice. If we end up choosing export
-before then that's what TS will use and the old grammer would only be used when - - experimentalDecorators
is set. We won't allow mixing and matching as the semantics are so different. Since you have to refactor anyways its less of a compatibility risk.
@mhegazy made a good point offline that not once in the almost-3 years since we shipped our decorators can we recall a complaint about the syntactic order of decorators with respect to the export
keyword. If possible we'd strongly prefer either the decorators-before-export
grammar (our current implementation), or the option for either style.
Since you have to refactor anyways its less of a compatibility risk.
As I understand it, decorator implementations can provide functionality that works under the older-style semantics as well as the newer-style semantics, so in those cases, you really are just providing a syntactic break with no provided semantic value.
It’s worth noting that the lack of a complaint with “before export” isn’t the same as the presence of one with “after export”, especially in the context of build-time verifications like TS affords.
Ultimately, I would prefer we only choose one, and not permit both, whatever the choice (I’ve expressed my preference above :-) )
Before I raised this issue, I searched related proposal repos to find usage of export with decorators, here are the results:
From https://github.com/tc39/proposal-decorators-previous/issues/35#issuecomment-323215132 by @zenparsing
@connect(...)
export class B {
render() {
// ...
}
}
From https://github.com/tc39/proposal-decorators-previous/issues/41#issue-260027055
@customElement('nav-bar')
export class NavBar {}
@useShadowDOM @customElement('my-expander') export class Expander { @bindable isExpanded = false; @bindable header; }
NOTE: This code example is coming from the old decorator draft: https://tc39.github.io/proposal-decorators-previous/#example-aurelia-customElement
3. From https://github.com/tc39/proposal-decorators-previous/issues/2#issuecomment-225314165 by @justinfagnani
```js
@define('my-element')
export class MyElement extends HTMLElement {}
All three use @deco export class
form.
I checked babel's implementation. It seems allowing only decorators-before-export.
$ cat .babelrc
{
"plugins": [
"transform-decorators-legacy"
]
}
$ cat test0.js
@deco export class A {
}
$ npx babel test0.js
var _class;
export let A = deco(_class = class A {}) || _class;
$ cat test1.js
export @deco class A {
}
$ npx babel test1.js
test1.js: Unexpected token, expected { (1:7)
Babel's implementation of the current decorator spec has not landed yet. The existing plugin is called -legacy
specifically because it doesn't reflect the current spec.
Babel 7.x's parser has a decorators2
plugin that implements the current proposal's placement of decorators after export
, and prints a helpful message if found beforehand, e.g.
Using the export keyword between a decorator and a class is not allowed. Please use
export @dec class
instead
for
@dec
export default class Foo {}
for example.
The 7.x transform does not yet expose this parser plugin for use, but we intend to as we transition to aligning with the current proposal.
Decos should be before export because that's how it reads better. We read the code more than we write it. This should be changed before it's too late.
I don’t agree; to me, before reads like it applies to the exporting; after reads like it applies to the thing being exported.
Correct me if I'm wrong but this is where it's going right now and I don't like it at all
export @Component({
template: `
<div>...</div>
`
}) @AnotherMeta({
x: ...
}) class Counter {
...
}
BTW: I think somebody will eventually write babel-plugin which will swap the order for you which means people will stick with @deco export ...
and the spec will get updated anyway
I originally thought it was weird to move the decorators between the export and class, but I’ve become used to it tho. I agree with both sides however.
The pattern I've come to is
export default
@decorator0
@decorator1()
@decorator2({meh: true})
class Foo extends Bar {
...
}
BTW: I think somebody will eventually write babel-plugin which will swap the order for you which means people will stick with @deco export ... and the spec will get updated anyway
This is a really unproductive thing to say.
This is a really unproductive thing to say.
While it's may unproductive, I see it as helpless words.
To be fair, in the history of JavaScript and Web platform standardization, most decisions are made by a few people, or vendors. When they did some unproductive things (I don't want to give examples here), the programmers were almost helpless. So I hope we could use our sympathy, and be tolerant to the unproductive thing from the programmers.
@hax the people making the decisions aren't saying "too bad, we're doing it anyways" - they simply might have reasoning that some programmers disagree with. That comment was purely unproductive, and feeling frustrated doesn't mean it's a good idea to go down the road of "i'm going to force my preference on you if you don't give me what i want" (not that it would be possible to do that with babel, nor that babel users' preferences could force an unwise spec change)
Sorry, I just wanted to point out, that babel is here and doing it is super easy, it's like 30 minutes of hacking, npm publish and voila, you can use deco before export even if it's not in spec.
And if there is enough people doing angular and writing templates (a lot of lines) in decorators (which I believe there is) somebody will eventually do it.
You can have many decorators (with complex params) and in my opinion, decorators look better when they're stacked. And because export can't be on its own line (bc of parsing), the only nice way is to move the whole stack of decorators before export class
Or export in a separate statement.
@decorator0
@decorator1()
@decorator2({meh: true})
class Foo extends Bar {
...
}
export { Foo };
//or
export default Foo;
Sent with GitHawk
@cztomsik I think it's beneficial to have alignment between Babel and TC39 long-term. Fragmenting into lots of dialects on these syntax questions doesn't really help anyone. @loganfsmyth Thanks for putting in the effort to generate such a good error message in this case.
OFC it is. Anyway, I'd really love to hear a reasoning behind this decision because I was discussing this with a lot of people doing js everyday and every single one of them would prefer deco before export (because its less typing and duplication then exporting the class later)
@deco
@deco({...})
@deco
class Clz
export Clz
@cztomsik I don't quite understand--how is the decorator first less typing or duplication? It seems like the same amount of typing, just in a different order. Note that this form is supported (I believe):
export
@deco
class Clz
Anyway, given the activity on this issue, I'd like to review it with the other decorators co-champions before the May meeting for reconsideration.
Oh, I thought export
with following newline would cause SyntaxError and obviously I was wrong, sorry for that.
@littledan
how is the decorator first less typing or duplication?
I guess it's not about typing but the reading/understanding of the code. export
is about external interface, but decorators are implementation details. To understand what is exported, you just need to read "export TYPE NAME" (where TYPE is function, class, const, etc.) which give you all the information.
Normally we read the code in the order of interface, then details. With the decorators, it becomes
export // external api, but don't know what is exported now
@deco1 // declarative, but still implementation details
@deco2
@deco3
class Test { // local binding, but also external api if there is export keyword lines before
// details
}
or
@deco1 // declarative implementation details
@deco2
@deco3
export class Test { // external api, and Test is also the name for the local binding
// details
}
Inject implementation details between export
and TYPE NAME
seems switching more context when reading the code.
The implementation details are what’s exported.
@ljharb
The implementation details are what’s exported.
I don't think so. The code
import {Test} from './test.js'
const test = new Test()
can be parsed before the test.js
loading and execution, which means only names are exported. The implementation details are linked later.
More important, I'm talking about the mental model of the programmers. To be honest, decorators are more declarative which may convey the higher-level abstraction than normal implementation details like class body, but I assume most programmers will still see decorators are much more internal than class name and method signatures, because we need name and type info but should not rely on the implementation details when writing the codes.
Note, I myself do not have a very strong opinion on this issue, just try to use my empathy for those who prefer decorators before export.
@hax Agree (but I understand this is highly subjective)
When I wrote last comment, I just realize that this issue also affect other class elements. We do not have many modifier keywords now (only static
uptonow), but if we have, for example hidden
in class 1.1 proposal, TS-like readonly
as extension for field proposals, this will be much general issue:
export
@deco1 @deco2
class A {
readonly
@deco1 @deco2
prop
hidden
@deco1 @deco2
method() {...}
static
@deco1 @deco2
method() {...}
}
or
@deco1 @deco2
export class A {
@deco1 @deco2
readonly prop
@deco1 @deco2
hidden method() {}
@deco1 @deco2
static method() {}
}
or have different preferences in each cases? (I believe current proposal is close to the latter but make export
as the only exception.)
I would prefer decorators to come before exports
. And I subscribe the previous arguments from @hax
Thinking about methods and properties with modifiers like static or whatever..
Decorators should be on top of everything so you're able to write it in a way that you can remove the whole line without dealing with language principles
At this point, I am leaning towards the decorator coming before the export keyword, following @rbuckton's and @k1r0s's arguments.
We discussed this issue in the decorators champions call, and some other people seemed convinced as well. We will revisit this to make a more solid recommendation to TC39. @rbuckton signed up to write spec text for this syntax.
@rbuckton @nicolo-ribaudo and I discussed this issue. For the reasons described above, we're all in favor of the change. We need to follow up with @wycats on the possibility of future export decorators, but at the same time, @rbuckton has described a good path for the combination above (if we ever figure out what export decorators might do). Ron volunteered to write a patch for the syntax change.
@wycats @bterlson @rbuckton and I discussed this issue again. At this point, we're all convinced that we should make this change. @rbuckton signed up for writing the patch.
@jsg2021 Sorry for being off topic in 92 following your comment...
How will you decorate "export" when needed if decorating a class before export is retained ?
@doodadjs See https://github.com/tc39/proposal-decorators/issues/69#issuecomment-370239981 for a syntax by @rbuckton .
@littledan Seen that, and I disagree.
To be more precise, if I want to decorate my house, I'll not decorate my neighborhood's house, with a link to my own house...
or
or allow both?