Open
Description
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:
- Deprecated/Obsolete, to support warning on use of specific API’s:
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;
}
- Suppress linter warning:
@@suppressWarning("disallow-leading-underscore")
function __init() {
}
Proposal
Design-time (Ambient) decorators are:
- ambient functions
- have a special name starting with "@"
- are not emitted in output JS code, but persisted in .d.ts outputs.
Application
- Applying a design-time can only accept constant values. Variables would not be observable by the compiler at compile time. Here is the set of possible values:
- string literal,
- number literal,
- regexp literal,
- true keyword,
- false keyword,
- null keyword,
- undefined symbol,
- const enum members,
- array literals of one of the previous kinds,
- object literal with only properties with values of one of the previous kinds
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
matjaz commentedon May 12, 2015
what is the benefit of using
ngdoc
@deprecated
https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentationbradolenick commentedon Jun 5, 2015
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 commentedon Dec 24, 2015
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 commentedon Jan 22, 2016
@kitsonk can you elaborate on your proposal?
kitsonk commentedon Jan 22, 2016
@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:
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 commentedon Jan 22, 2016
Just a clarification; this example you still need to mixin
A
andB
intoC
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 commentedon Jan 22, 2016
@mhegazy yes... my specific use case would be to mirror run-time operations which are difficult/impossible to currently do in TypeScript. For example:
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 commentedon Jan 23, 2016
Risking to derail this issue, but just wondering if you looked at the proposal in #6502 (comment), and what you think about it.
kitsonk commentedon Jan 23, 2016
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).
58 remaining items
kingsimba commentedon May 21, 2020
With this feature, is it possible to write something like:
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.0shicks commentedon Sep 15, 2020
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 contentiousfinal
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 commentedon Nov 14, 2020
My use case for this would be to annotate some of the public methods as "experimental" which means that may change in the future.
Jarred-Sumner commentedon Feb 21, 2021
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 commentedon Apr 25, 2022
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 namespacefoo
for two specially-named types:foo.PERSIST_IN_DECLARATIONS
andfoo.OMIT_AT_RUNTIME
. If either/both istrue
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 commentedon Mar 21, 2023
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 commentedon Mar 24, 2023
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;
}
`
Now I have to rely on my own GUI tool in my IDE for developers to add attributes(decorators).
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 commentedon Mar 24, 2023
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 commentedon Mar 25, 2023
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 commentedon Mar 25, 2023
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:
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:
const myDecorator: (<T extends new() => any>(ctor: T) => T) & DesignTag = (ctor) => ...
but it would be doable. TheDesignTag
could benefit from Tag types #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.
weiqj commentedon Mar 25, 2023
I can give a use case for decorating type-only elements (such as interfaces), which will show how much bigger TypeScript can go beyond JavaScript.
Let's use the code I posted earlier as an example. The code will be transpiled into Lua instead of JavaScript. The interfaces are just declarations of data structures, which will be arguments of a main() entry function.
What it does is that it allows the automatic generation of a Tree UI on any process, which I call "Apps." Those apps are not phone Apps because they don't run on phones. They are IoT Apps.
That particular code defines the input structure (configuration) of a "Smart Sprinkler." It looks like this on an end-user's smartphone. It's up to the end-user to create an instance of the interface data (tree data) and use the data to create the actual process.
So what does the decorator do? It provides additional information to optimize the generated Tree UI.
Of course, another developer can write a more "traditional" sprinkler app with a strict time scheduler.
I am also working on the App engine to power the tiny little wireless MCU chips. A chip with 256KB of RAM can run 10000 lines of TypeScript code. Yes, it's 256KB, several 100,000 times less RAM than your smartphone.
TypeScript can be truly "write once, run everywhere" on trillions of devices!
hydroper commentedon Jun 16, 2023
The only workaround I think of since we don't have this feature yet is...
Attaching a related meta-data file to another .ts file
For example, say you've a transformer that compiles TypeScript to WebAssembly and want to export a TypeScript function named
Q.fooFn
as a function namedq_foo_fn
in a target WebAssembly module. You could do this as follows:fooFn.ts
fooFn.ffi.json
I don't know how symbols work in the TypeScript Compiler API yet, so I don't know how resolve
Q.fooFn
relative to thefooFn.ts
module.