microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.28k stars 12.39k forks source link

Add Support for design-time decorators #2900

Open mhegazy opened 9 years ago

mhegazy commented 9 years ago

Now that decorators are supported in TypeScript (#2249), consider adding support for ambient/design-time-only decorators:

Ambient decorators can be an extensible way to declare properties on or associate special behavior to declarations; design time tools can leverage these associations to produce errors or produce documentation. For example:

Use cases:

interface JQuery {  
    /**  
     * A selector representing selector passed to jQuery(), if any, when creating the original set.  
     * version deprecated: 1.7, removed: 1.9  
     */  
    @@deprecated("Property is only maintained to the extent needed for supporting .live() in the jQuery Migrate plugin. It may be removed without notice in a future version.", false)  
    selector: string;  
}
@@suppressWarning("disallow-leading-underscore")   
function __init() {  
}

Proposal

Design-time (Ambient) decorators are:

Application

matjaz commented 9 years ago

what is the benefit of using ngdoc @deprecated https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation

bradolenick commented 9 years ago

For this to be useful in our scenario, we need ambient decorators to be preserved in the generated .d.ts.

In our scenario, compiling a set of .ts files produces a library consumed by others via the library's .d.ts (conventional stuff so far). Those who consume the library, compile against it using a custom tool built in terms of the TypeScript compiler APIs. The tool makes use of ambient decorators in the library's .d.ts.

Today, we're prototyping this tool in terms of 1.5beta decorators, but ambient decorators would allow us to expand to decorate non-classes/non-functions.

kitsonk commented 8 years ago

I noticed that this is on the roadmap for 2.0.

Would it be another use-case to allow augmentation of other design time features (like typing)? Something like augmenting the reflect meta-data (@rbuckton).

This is part of my never ending quest to be able to get effective design time mixin/trait support while we all wait for TC39 to figure something out.

mhegazy commented 8 years ago

@kitsonk can you elaborate on your proposal?

kitsonk commented 8 years ago

@mhegazy I might not be the best person to expound upon the proposal, as I have only lived in JavaScript land for an extended period of time (and Delphi a long time ago) and some of the Reflection and Type handling that are part of C# and other languages I am not familiar with.

I guess what I am suggesting is that at design time, in these decorators, some of the internal TypeScript constructs would be available. For example, the ability to construct and return a new type, similar to the C# TypeBuilder.CreateType

But I guess from my perspective, if I had some programmatic design time access to types via these design-time decorators, I could implement language features (mixins/traits) without needing syntatical support in typescript. From a "vision" objective, I would see something like this:

function mixin(target: any, ...mixins: any[]): type {
    const typeTarget = typeof target;
    mixins.forEach((mixin) => {
        const typeMixin = typeof mixin;
        for (let t in typeMixin) {
            typeTarget[t] = typeMixin[t];
        }
    });
    return typeTarget;
}

class A {
    foo() { return false; }
}

class B {
    bar() { return 'bar'; }
}

@@mixin(A, B)
class C {
    baz() { return 1; }
}

const c = new C();
console.log(c.foo(), c.bar(), c.baz());

Of course, I could put a lot of different logic in there, but essentially I could perform design-time operations on the types. I don't know the inner workings of the compiler, but I suspect there would be good set of semantics for safely programmatically operating on types.

mhegazy commented 8 years ago

Just a clarification; this example you still need to mixin A and B into C at runtime as well; so if understand correctly, this is just a design time mutation to allow the compiler to model what will happen at runtime?

kitsonk commented 8 years ago

@mhegazy yes... my specific use case would be to mirror run-time operations which are difficult/impossible to currently do in TypeScript. For example:

class A {
    foo: string = '';
}

class B {
    foo() { return 'foo'; };
}

interface GenericClass<T> {
    new (...args: any[]): T;
}

function mixin<T, U>(target: GenericClass<T>, source: GenericClass<U>): GenericClass<T&U> {
    return;
}

const C = mixin(A, B);

const c = new C();

c.foo; // is of type string & (() => string)

Clearly, the intent of my code was not to create a type of string & (() => string) and my mixin function would have resolved that in some way. I could use the same resolution logic at runtime with my decorator and safely resolve the type. Currently, I would typically "overlay" and interface on my final class which resolves any conflicts.

I am sure there are other use cases, where it is difficult/impossible to express the actual run-time types without some advanced design-time logic to mutate those types. extends, union and intersection aren't always sufficient.

mhegazy commented 8 years ago

Risking to derail this issue, but just wondering if you looked at the proposal in https://github.com/Microsoft/TypeScript/issues/6502#issuecomment-173312747, and what you think about it.

kitsonk commented 8 years ago

No, I had missed it, as I mentioned there, I do think it is related to #3870 (which this is sort of in the same space as well), but I think the challenge comes in that sometimes we can (want/need/desire) to do things at runtime that require us to express some heavy design time logic. I see design time decorators too as a potential way of avoiding introducing some challenging language semantics. For example in #6502, the proposal is to place syntax that would have to both be erased but also add more to the emit, which could easily cause forward incompatibilities with ES. Property initialisers are likely to come to ES (YAY!) but what is the likelyhood that the proposed syntax in #6502 would be aligned? I would expect the TypeScript team to decline that just like #311.

The beauty of design time decorators is that, as the name implies, they would be 100% erasable with almost no need to consider the emit (except when emitting meta data).

silkentrance commented 8 years ago

Design time custom decorators/annotations just as in Java / C# would require the transpiler to examine external sources, which might already have been transpiled, as such these design time decorators/annotations will have already been removed from these sources. So in my personal understanding this is not an option unless one wants to go for bytecode and Java / C# like package constructs.

And as far as my understanding of existing transpilers such as Babel and maybe also TypeScript goes, these only examine a single source file at a time and will rewrite that file, not considering any external files that might be imported, say using the import statement or, more hidden, the require() method.

kitsonk commented 8 years ago

Design time custom decorators/annotations just as in Java / C# would require the transpiler to examine external sources

Not necessarily. There is a set of typing meta data that is already inferred by the time the decorator would be invoked. TypeScript makes assumptions about the structure of external files all the time, which is how it accomplishes the intellisense when integrated into IDEs such as VSCode, using a number of different methods (including ambient declarations). This would essentially allow code to interface with that information direction at design time. Most of that is already in TypeScript type structures, since TypeScript is compiled with TypeScript.

silkentrance commented 8 years ago

@kitsonk you are talking about IDE integration here, which is an altogether different thing.

As for decorators, please compare https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Decorators.md to what is defined in here and you might see that it is actually the same.

As for design time vs. compile time vs. runtime. These are very different things. And just because the typescript compiler is able to compile itself, does not necessarily mean that it is capable of loading external entities and make that part of the AST during compilation.

kitsonk commented 8 years ago

@silkentrance @mhegazy is one of the core team working on TypeScript. I am pretty sure he understands the difference between design time and runtime decorators and how decorators work in TypeScript.

Actually IDE integration and how TypeScript compiles and emits code aren't very different. TypeScript gathers a whole lot of information, including transforming to an AST with appropriate typing information to be able to offer up Intellisense or code emitting.

And just because the typescript compiler is able to compile itself, does not necessarily mean that it is capable of loading external entities and make that part of the AST during compilation.

I am afraid it can and it does.

silkentrance commented 8 years ago

@kitsonk so even if typescript was able to infer certain properties from an already transpiled source, it would have to annotate that source by certain means. looking at transpiled sources of typescript, I cannot find such annotations, though, it is still plain javascript.

As such, VCode or whatever the name of the IDE was, requires the original sources by which it is able to infer such idioms of the TypeScript language.

See the compiled source in the PLAYGROUND

kitsonk commented 8 years ago

With plain JavaScript (or already compiled TypeScript) TypeScript can understand the structure of those files using ambient declarations via .d.ts files. This is how the DOM and the rest of the built in APIs are typed. There are other tools out there can can generate the appropriate information so people can continue to build on-top of the emitted code. I happen to maintain once such tool. In those cases, TypeScript doesn't even read the source files.

As a side note, there is part of the TypeScript services called Salsa, which gives similar inferred typing information to plain JavaScript code being written in JavaScript. Those services can be enhanced with further data from .d.ts files as well and this is what is starting to power the intellisense in Microsoft Visual Studio Code (that "whatever" IDE I was referring to) when editing JavaScript files in that IDE.

Also, just to point out, this particular issue is part of the TypeScript Roadmap for 2.0 (the Ambient Declarations). So I am pretty sure the TypeScript know what they are talking about.

silkentrance commented 8 years ago

Considering ambient decorators further, there cannot be custom ambient decorators unless they are part of the same source file. As such, these need to be made part of the language. See https://github.com/wycats/javascript-decorators/issues/27

silkentrance commented 8 years ago

@kitsonk you are talking about extraneous files generated by the compiler that are not part of any specification so far as I can see. So while this is a special case/behavior of the typescript compiler, it cannot be made a general feature as requested in https://github.com/wycats/javascript-decorators/issues/27

felixfbecker commented 8 years ago

Does this add anything that cannot be done with a jsdoc tag, like @deprecated?

alexeagle commented 8 years ago

+1 for a decorator-based suppression mechanism. Now that tsc has additional control flow checks like --noImplicitFallthrough it would be great to have a general-purpose mechanism for suppression, both for third-party checks like tslint as well as first-party checks shipped with the compiler. For my use case, I want to enable new static analysis checks globally across a large codebase (eg. angular2) and a suppression mechanism is a good alternative to fixing the problem, in the rare cases where the behavior is expected. (like a test that checks the behavior of implicit fallthroughs)

alexeagle commented 8 years ago

cc @chuckjaz We would also like to treat Angular 2 decorators as 'design time' when we are compiling templates offline in a build step, since any emit for these decorators is dead code. They are fully converted by our codegen step.

alexeagle commented 8 years ago

Another very significant use-case has come up. We now recommend that Angular apps use a tree-shaker like Rollup or Closure Compiler to eliminate ES6 modules that are not reachable from an application's entry point(s). TypeScript's Decorator emit retains all of these symbols both in the global reflect-metadata, and also the tree-shakers observe that the decorator function may have arbitrary side-effects and cannot remove them. This means that tree-shaking is just broken with standard TC39 decorators.

(Jonathan Turner seems to have taken down his github page that is referenced in the spec proposal - https://github.com/wycats/javascript-decorators#notes - does anyone have a copy?)

mhegazy commented 8 years ago

The OP is pretty much what was in this gist. here is an earlier version that i found on my email archive:

C.6 Ambient decorators
-   These are decorators that are not emitted. This category include built-in decorator (decorator, conditional, deprecated, etc..)
-   For design-time decorator, arguments are restricted to constant values. Variables would not be observable by the compiler at compile time. Here is the set of possible values:
o   string literal,
o   number literal, 
o   regexp literal,
o   true keyword,
o   false keyword,
o   null keyword,
o   undefined symbol,
o   const enum members,
o   array literals of one of the previous kinds,
o   object literal with only properties with values of one of the previous kinds
-   Examples: @conditional, @deprecated, @profile
mhevery commented 8 years ago

@mhegazy I think we need something which can be switched using an external flag. If we use online compilation in Angular then we need the decorators to be preserved at runtime. On the other hand if we use offline compilation then we need to have the decorators to be ambient.

The implication is that @@Component would not work for us as it would break the runtime compilation mode, while @Component breaks the offline compilation mode (by defeating the tree shaker). What we need is a way to decide which decorators are ambient and which are runtime using some form of external flag.

mhegazy commented 8 years ago

If we use online compilation in Angular then we need the decorators to be preserved at runtime.

looks like an emit time extension, that can decide to strip out all decorators at compile time, and if that is on run, e.g. in case of online compilation, decorators would still run as they do today.

alexeagle commented 8 years ago

We already pre-released our ngc compiler which does that emit time extension: https://github.com/angular/angular/blob/master/modules/%40angular/compiler_cli/src/compiler_host.ts#L48 note that it currently strips all decorators, but instead it should only strip the ones that our template compiler will do code-gen for. Otherwise users are constrained to use statically-analyzable decorators in their entire app and all libraries. This is where a Decorator on the Decorator would help:

// Strawman
@StaticallyAnalyzable
@AngularTemplateCompilerAware({options?: any})
function ComponentFactory {}

along with a compile-time option --emitForAngularTemplateCompiler which would turn on the custom Decorator lowering.

Misko and I need to discuss some more so we have a coherent proposal for this.

On Mon, May 9, 2016 at 11:30 AM Mohamed Hegazy notifications@github.com wrote:

If we use online compilation in Angular then we need the decorators to be preserved at runtime.

looks like an emit time extension, that can decide to strip out all decorators at compile time, and if that is on run, e.g. in case of online compilation, decorators would still run as they do today.

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/2900#issuecomment-217948618

silkentrance commented 8 years ago

@alexeagle Why do you not just decorate the decorators of your framework, and for which you will generate code for, with a specific decorator?

In Java, there are annotations which have a specific retention policy, e.g.

https://deors.wordpress.com/2011/10/08/annotation-processors/ https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/RetentionPolicy.html

So my best guess would be to annotate your decorators with for example

// Strawman
//@ambient
function AngularDecorator
{}

@AngularDecorator
function ComponentFactory() {}

And then check in your code emitter whether the decorator has been decorated using the @AngularDecorator, if not keep it, otherwise eliminate it, depending on the mode in which the compiler runs.

In turn, it will also eliminate the @AngularDecorator function itself.

alexeagle commented 8 years ago

Yes, thanks for the corrections, and yes Java's annotation retention is part of my background as well. We could already do this ourselves, I think the point of this issue is to search for a design which serves our needs as well as other TypeScript users. Perhaps this means a tsc option which allows any meta-decorated decorator to have static annotation-style emit. Will sync with Misko and give a more complete proposal...

On Mon, May 9, 2016 at 1:13 PM Carsten Klein notifications@github.com wrote:

@alexeagle https://github.com/alexeagle Why do you not just decorate the decorators of your framework, and for which you will generate code for, with a specific decorator?

In Java, there are annotations which have a specific retention policy, e.g.

https://deors.wordpress.com/2011/10/08/annotation-processors/

https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/RetentionPolicy.html

So my best guess would be to annotate your decorators with for example

// Strawman //@ambient function AngularDecorator {}

@AngularDecorator function ComponentFactory() {}

And then check in your code emitter whether the decorator has been decorated using the @AngularDecorator.

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/2900#issuecomment-217975746

silkentrance commented 8 years ago

@alexeagle here you lost me :grin:

alexeagle commented 8 years ago

For our special decorators, we don't drop them altogether, we just emit code that looks like static annotations (the metadata is attached directly as properties on the function{

On Mon, May 9, 2016 at 1:42 PM Carsten Klein notifications@github.com wrote:

@alexeagle https://github.com/alexeagle here you lost me [image: :grin:]

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/2900#issuecomment-217983359

unional commented 8 years ago

Should we consider using this in place of directive to solve the interop problem? https://github.com/Microsoft/TypeScript/issues/7398

SergeyTeplyakov commented 8 years ago

@mhegazy Our team in TSE would be really grateful if this feature would be implemented.

cspotcode commented 7 years ago

Another use case is enforcing an interface for the static side of a class.

declare function @StaticImplements<T>(t: T): T;
interface Foo { prop: string; }
@@StaticImplements<Foo> class MyClass { static prop = 'hello world'; }
shlomiassaf commented 7 years ago

@mhegazy I understand why we need to pass constant value but why can't we pass lambda or functions? They are constant after all.

The logical barrier is design time vs run time data, if the function does not access variables outside of it's block it safe.

Seeing the list you specifically mentioned regexp literal I guess you mean that TypeScript will not eval the object that the decorator wraps.

Still, we can let the user code handling the input eval that code...

The first use case I can think of is sending instructions to the Angular AOT compiler, this is not possible at the moment which makes implementing custom decorators of angular DI elements / Components a problem.

Angular can work around that by coupling the core module with all decorators to the AOT compiler and inside declare those.

Another use case for AOT compiler is mixins, where you return a value from a function that gets a class, that class could not be a part of DI unless the AOT compiler can get notified that the type is the param sent to the mixin.

jods4 commented 7 years ago

Now that TS 2.3 has shipped an API for language plugins, I think this should get a priority boost.

Once you start adding framework-specific analyzers or language extensions, the need for additional metadata arises very quickly.

Basic decorators can be used for that, but they have the downside of emitting useless code. Even more so when combined with --emitDecoratorMetadata.

alexeagle commented 7 years ago

Note that Angular has had our own solution for design-time decorators (things like angular @Component are stripped before downleveling). We should totally generalize that as a language service plugin.

On Thu, Apr 27, 2017 at 2:13 PM jods notifications@github.com wrote:

Now that TS 2.3 has shipped an API for language plugins, I think this should get a priority boost.

Once you start adding framework-specific analyzers or language extensions, the need for additional metadata arises very quickly.

Basic decorators can be used for that, but they have the downside of emitting useless code. Even more so when combined with --emitDecoratorMetadata.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/2900#issuecomment-297841025, or mute the thread https://github.com/notifications/unsubscribe-auth/AAC5IzEVTI_lAG5cpxm-QfT2MaI5KJI5ks5r0QTqgaJpZM4EHdy2 .

jods4 commented 7 years ago

(things like angular @Component are stripped before downleveling). We should totally generalize that as a language service plugin.

Very interesting... It is my understanding that transforming the AST or codegen is explicitly a non-goal of Language Service plugins, so how would you do that?

I am very interested in being able to add/remove stuff from the generated JS code, including when building with tsc, but that seems out of the picture for now.

To the TS team: it's sad that we can't transform TS code, because transforming the downlevel JS code is much harder after types have been erased :(

EDIT: and it's sad we can't even add diagnostics when building with tsc. A plugin for a templating framework (say Angular) can add warnings/errors for incorrect binding expressions in the IDE, but if you then go on to build this in your CI everything is green. Same for linting: it's great to now have immediate IDE feedback, but why do we need to run it separately on the command line?

alexeagle commented 7 years ago

Sorry I typed without thinking - we could generalize our solution as a transformer. Angular (and Bazel) run typescript through our own CLI program, not tsc, so we can add transformers. You can add a transform at the beginning of the pipeline while the sources are still TS is my understanding.

On Thu, Apr 27, 2017 at 2:35 PM jods notifications@github.com wrote:

(things like angular @Component are stripped before downleveling). We should totally generalize that as a language service plugin.

Very interesting... It is my understanding that transforming the AST or codegen is explicitly a non-goal of Language Service plugins, so how would you do that?

I am very interested in being able to add/remove stuff from the generated JS code, including when building with tsc, but that seems out of the picture for now.

To the TS team: it's sad that we can't transform TS code, because transforming the downlevel JS code is much harder after types have been erased :(

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/2900#issuecomment-297845853, or mute the thread https://github.com/notifications/unsubscribe-auth/AAC5I5cXGdHbCmIkOpF44kbREW9XEitCks5r0QoHgaJpZM4EHdy2 .

brn commented 7 years ago

Any update on this issue? I'm come from Roadmap of wiki that is still written about Design-Time-Decorator. Is CustomTransformer covered this issue? or continue to investigate this issue?

Nufflee commented 6 years ago

I'm interested in implementing this (and have already started experimenting with the source code) but I'm not sure what the state of this issue is and if it's worth to open a pull request. I know I'm not supposed to open a pull request unless a issue's milestone is "Community" but in the roadmap it states "Investigate Ambient(, Deprecated, and Conditional) decorators" so I would like some clarification here. This is also mentioned in #20724 as "One day?".

thetutlage commented 4 years ago

I see this issue being mentioned in the roadmap, so I hope this will get some attention soon. Just wanted to check, will design time decorators be able to define custom return types for a function? Example: https://github.com/microsoft/TypeScript/issues/4881#issuecomment-142131626

kingsimba commented 4 years ago

With this feature, is it possible to write something like:

@@expose({name: "sockFile", since: 2.0})
domainSocketFile: string

It should be kept in .d.ts file. And it tells me that when serialize to JSON, it will be renamed to "sockFile", and it will only be serialized after version 2.0

shicks commented 4 years ago

I would also like to see this gain some traction. Here are a few use cases that we currently don't have a good solution for:

Part of the opposition to final as a built-in language feature is an argument that it can be implemented as a runtime check outside the language, but we all appreciate that compile-time checks are often preferred. Unfortunately, with ordinary decorators leaving no artifact in the .d.ts emit, it's impossible for a compile-time checker to see that an extended class or overridden method was annotated @final. (In this case, having both a design-time and runtime emit would be ideal, but if I can only get one, the .d.ts emit has much higher value). If we can address the contentious final issue with a design-time decorator, that might significantly decrease the unhappiness of the many people asking for it.

@@checkReturnValue also needs to be visible in the .d.ts since the primary use case is for communicating a library's API to the library's users.

aminya commented 3 years ago

My use case for this would be to annotate some of the public methods as "experimental" which means that may change in the future.

class myClass {
    @@experimental("This method is experimental and may change in the future versions")     
    doSomething() {
    }
}
Jarred-Sumner commented 3 years ago

I wrote an esbuild plugin today that kind of implements design-time decorators: https://github.com/jarred-sumner/decky.

It uses the existing @decoratorName(arg) syntax though, so that Prettier/etc still work and has a different interface for writing decorators to support the constraints of code that only runs at build-time (doesn't use a JavaScript AST). The syntax is similar enough to fool TypeScript into thinking usages of the decorators are just runtime decorators.

shicks commented 2 years ago

Now that decorators have advanced to Stage 3, I wonder if there's anything else to say about this?

As I mentioned above, I'd like to see a way to get both runtime and design time effects out of a single @final decorator. I assume that some information about any given decorator should be available when type-checking downstream code that uses the decorator. Is there some way we could annotate an ordinary decorator as "please persist in declarations"? To my mind, the question of whether to emit at runtime is mostly orthogonal (aside from the fact that omitting a decorator from both would have very limited value). So it's not clear to me that progress on this is blocked on some special syntax making its way through TC39 (and I'm also skeptical of whether it would even be able to do so, given that if it has no runtime effect, it's effectively no different from a comment, and there's been a lot of pushback on types-as-comments).

A straw-person proposal (it's admittedly pretty ugly, but we have to start somewhere): when compiling @foo class Bar { ... }, look at the namespace foo for two specially-named types: foo.PERSIST_IN_DECLARATIONS and foo.OMIT_AT_RUNTIME. If either/both is true then the compilation will do the appropriate thing when producing .d.ts or .js output. This can only really be applied to top-level imported decorators (so that both the namespace and the value are in scope with the same name), but that should be fine - there's not really value to persisting local decorators (i.e. from a function parameter) to declarations. This proposal causes zero runtime overhead and is doable today without any further upstream standards.

weiqj commented 1 year ago

TypeScript 5.0 no longer allows many decorators since switching to stage 3 decorators support. For example, a decorator on an interface member now results in a "MissingDeclaration" kind. Is there any plan to switch to legacy behavior?

weiqj commented 1 year ago

I feel that I have to say something. Since the stage 3 decorator change, TypeScript has removed decorator support for interface members. So instead of writing something like this: ` declare class SprinklerZone { @LibertasFieldAccess(LibertasAccess.Write) @LibertasFieldUnique() @LibertasFieldTableHeader() @LibertasFieldDeviceTypes([ [ LibertasDeviceLoadType.LOAD, LibertasDeviceId.SPRINKLER_CONTROLLER, [LibertasClusterId.GEN_ON_OFF] ] ])
sprinklerZone: LibertasDevice;

@LibertasFieldMin(50)
@LibertasFieldMax(150)
@LibertasFieldStep(10)
@LibertasFieldDefault(100)
fieldCapacity: number;

@LibertasFieldDefault("Loam")
soilType: SoilType;

@LibertasFieldDefault("Lawn")
plantType: PlantType;

@LibertasFieldDefault("PopupSpray")
head: SprinklerHead;

} ` Now I have to rely on my own GUI tool in my IDE for developers to add attributes(decorators).

sprinkler_zone

Some may say that it is a better way. It is more robust because I have 100% control. And young developers prefer that way. And it helps me with vendor lock-in. No one seems to lose anything.

But from a purely technical point of view, it still sucks. I don't want to see a good invention just refuse to be something much bigger but choose to be a puppet of JavaScript instead.

rbuckton commented 1 year ago

TypeScript 5.0 no longer allows many decorators since switching to stage 3 decorators support. For example, a decorator on an interface member now results in a "MissingDeclaration" kind. Is there any plan to switch to legacy behavior?

We've never supported decorators on ambient classes or interfaces at compile time. The only reason they existed within the AST was to report grammar errors.

weiqj commented 1 year ago

TypeScript 5.0 no longer allows many decorators since switching to stage 3 decorators support. For example, a decorator on an interface member now results in a "MissingDeclaration" kind. Is there any plan to switch to legacy behavior?

We've never supported decorators on ambient classes or interfaces at compile time. The only reason they existed within the AST was to report grammar errors.

I am very well aware of that. But the fact is that it was in AST, and now it is gone. It worked perfectly for my purpose while it was on AST. The warning can be easily suppressed.

I was just giving my personal opinion. I believe it was in the best interest of TypeScript as a general-purpose programming language with many unique features.

shicks commented 1 year ago

Rather than focusing on a particular use case that wasn't well-supported (anywhere you need to suppress an error seems reasonably fair-game to be broken in the future), it's probably more constructive to think about how design-time decorators could be handled more generally.

From my perspective, I see two broad possibilities:

  1. Find a way to tell TS to treat a TC39 runtime decorator as design-time, or
  2. Invent new syntax for design-time decorators.

For various reasons, I think (1) is a more promising alternative: first, because (as I've pointed out above) it's quite reasonable for a single decorator to have both runtime and design-time behavior, and second, because my impression is that the TypeScript team would be hesitant to move on (2) without upstream (TC39) consensus on JS syntax, and this seems unlikely given that it would effectively be a comment, and there doesn't seem to be much traction to adding syntax that doesn't actually affect runtime at all.

I can imagine a couple ways that a decorator could be "marked" as design-time, which wouldn't require new JS syntax:

  1. Teach TS to recognize decorators that augment the type of their input. Outright changing the type seems quite dangerous, but allowing a decorator to return a subtype of the input (i.e. adding properties to a class, or intersecting a property's type with a tag of some sort) would be quite a bit more principled and should be relatively safe.
  2. Allow the decorator itself to be tagged at the type level. This would be a bit more awkward to write, given that you'd need to do something like const myDecorator: (<T extends new() => any>(ctor: T) => T) & DesignTag = (ctor) => ... but it would be doable. The DesignTag could benefit from #4895 by being a standard-library tag type.

There are certainly other options, and I imagine we would do well to brainstorm it a bit. This is unlikely to gain any traction without a compelling proposal. But if there were a working use case by which decorating type-only elements (such as interfaces) would actually be meaningful (e.g. because it would affect the .d.ts emit), then it doesn't seem unreasonable that such behavior could be actually supported, rather than just a suppressed error.