-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Request: Class Decorator Mutation #4881
Comments
Same would be useful for methods: class Foo {
@async
bar(x: number) {
return x || Promise.resolve(...);
}
} The async decorator is supposed to change the return type to |
@Gaelan, this is exactly what we are needing here! It would make mixins just natural to work with. class asPersistent {
id: number;
version: number;
sync(): Promise<DriverResponse> { ... }
...
}
function PersistThrough<T>(driver: { new(): Driver }): (t: T) => T & asPersistent {
return (target: T): T & asPersistent {
Persistent.call(target.prototype, driver);
return target;
}
}
@PersistThrough(MyDBDriver)
Article extends TextNode {
title: string;
}
var article = new Article();
article.title = 'blah';
article.sync() // Property 'sync' does not exist on type 'Article' |
+1 for this. Though I know this is hard to implement, and probably harder to reach an agreement on decorator mutation semantics. |
+1 |
If the primary benefit of this is introducing additional members to the type signature, you can already do that with interface merging: interface Foo { foo(): number }
class Foo {
bar() {
return this.foo();
}
}
Foo.prototype.foo = function() { return 10; }
new Foo().foo(); If the decorator is an actual function that the compiler needs to invoke in order to imperatively mutate the class, this doesn't seem like an idiomatic thing to do in a type safe language, IMHO. |
@masaeedu Do you know any workaround to add a static member to the decorated class? |
@davojan Sure. Here you go: class A { }
namespace A {
export let foo = function() { console.log("foo"); }
}
A.foo(); |
It would also be useful to be able to introduce multiple properties to a class when decorating a method (for example, a helper that generates an associated setter for a getter, or something along those lines) |
The react-redux typings for |
I think the Currently it's
Obviously the naming sucks and I have no idea if this sort of thing will work (I am just trying to convert a Babel app over to typescript and am hitting this). |
@joyt Could you provide a playground reconstruction of the problem? I don't use react-redux, but as I've mentioned before, I think any extensions you desire to the shape of a type can be declared using interface merging. |
@masaeedu here is a basic breakdown of the moving parts.. Basically the decorator provides a bunch of the props to the React component, so the generic type of the decorator is a subset of the decorated component, not a superset. Not sure if this is helpful, but tried to put together a non-runnable sample to show you the types in play.
If you want a full working example I suggest pulling down https://github.com/jaysoo/todomvc-redux-react-typescript or another sample react/redux/typescript project. |
According to https://github.com/wycats/javascript-decorators#class-declaration, my understanding is that the proposed |
The spec says:
is translate to:
So if I understand it correctly, the following should be true:
|
For your example:
I'm assuming you mean that declare function F<T>(target: T): typeof X; For that case, the assertions should be: let a: X = new Foo(); // valid
let b: Foo = new Foo(); // valid The let Foo = F(class Foo {}); |
@nevir Yep, you are right. Thanks for clarification. |
On a side note, it seems like turning off the check to invalidate mutated class types is relatively easy:
But I am not knowledgable enough to make the compiler output the correct type definitions of the mutated class. I have the following test: tests/cases/conformance/decorators/class/decoratorOnClass10.ts
It generates tests/baselines/local/decoratorOnClass10.types
I was expecting |
For those interested in react-redux's |
WE NEED THIS!! |
No we don't. We don't need decorators. Strictly speaking, they are not needed. They are another take on the higher-order function pattern thar has been used for decades and is still more powerful - and typed. |
@ericmorand please replace |
@ericmorand Strictly speaking, you don't need a computer in the first place. 😕 |
That's fallacious and you know it. You need a computer to actually execute computing software. But you don't need decorators - and Class Decorator Mutation - to write and execute computing software...or to compose functions together. Now, the fact that an issue opened for 8 years, and that asks for something that is not needed at that is natively available using functional programming since the beginning of the existence of TypeScript, is something that should trigger some thoughts: if, for the last 8 years, you have been not typing your factories mutations because you wait for decorators and classes to support this, the problem is on you to refuse to use the features that TypeScript proposes; not on TypeScript. |
Interesting that you didn't make the same answer to @Marckon when they wrote:
;) |
It's been waiting for decorators to officially be supported. That has only recently been the case (TypeScript 5.0).
I can assure you, TypeScript 0.8 did not support this even in the slightest, and neither did many many later versions. Otherwise I'd ask people to please stop with the off topic comments. It's getting really spammy in the mail notifications for subscribers with pointless or plain opinionated comments. |
+1 |
We have a use case for this that I thought I'd articulate here - hopefully it's useful context and not just a +1 😜. A growing number of frameworks feature a class-based component model, with decorators used for component registration. In our work, we have such a model, but it's also true for Angular and StencilJS. In general the format is this: @Component({ /* configuration options */});
class MyComponent {
// ...
} This works well for configuration and registration of the component at runtime. We're building an application that uses type information (directly from the TS language services API) to understand the nature and structure of just such a component model. Users write their components as above, then we present a UI that users can use to compose and edit a component structure. The issue we have is that the type information inside the While this is indeed currently possible with a function, it's syntactically cumbersome, especially for established component models that use decorators (such as those above). |
@cgauld, I know you find "syntactally cumbersome" to use decorators as functions, but it really is the solution, one that doesn't require any change in the compiler to work, and definitely one that is not cumbersome by any mean and that does respect the principle of single responsibility. type Constructor<Instance> = {
new(...args: any[]): Instance;
};
function Component<C extends Constructor<any>>(constructor: C, options?: {
foo: number;
}): Constructor<InstanceType<C> & {
foo: number;
}> {
return class extends constructor {
get foo() {
return options?.foo | 1;
}
};
}
// let's use the decorator as a syntax
@Component
class MyComponent {
bar: 'bar';
}
const component = new MyComponent();
component.bar;
component.foo; // TS2339
// let's use the decorator as a function
export const MyOtherComponent = Component(MyComponent);
const otherComponent = new MyOtherComponent();
otherComponent.bar;
otherComponent.foo; // fine
``` |
@ericmorand to be fair, that assumes you have control over the framework/library consumer code, which is not always the case. And to @cgauld's point, Angular and StencilJS are pretty well established, so it can be difficult to convince developers who use such a framework to refactor from class decorators to function calls. Especially since many developers don't understand what the decorators are doing, they just follow the documentation. To generalize even further, while abstractly decorators can be viewed as just another take on higher-order functions, from a practical perspective, they can make a meaningful difference in making code more readable and accessible, especially for newer developers. There is a reason many widely used libraries have chosen to utilize decorators, and that TypeScript chose to experimentally support decorators long before they were at Stage 3 in ECMAScript. And let's be honest, unless TypeScript chooses to deprecate support for decorators, it is not reasonable to say the proper solution to avoid this bug with decorators is by not using decorators. If the feature is intentionally supported in TypeScript, it should work properly. Currently, the type of a decorated class is just wrong. E.g.: type Constructor<Instance> = {
new (...args: any[]): Instance
}
const Component = <C extends Constructor<any>>(constructor: C, context?: ClassDecoratorContext): Constructor<InstanceType<C> & {
foo: 'unexpected value'
}> => {
return class extends constructor {
foo: 'unexpected value' = 'unexpected value'
}
}
@Component
class MyComponent {
foo: 'foo' = 'foo'
}
const component = new MyComponent()
if (component.foo !== 'foo') {
throw new Error('How is this possible? MyComponent.foo is clearly typed as \'foo\'')
} |
If this happens to be true then it is very sad and depressing. Aside of this, my point was that the decorators themselves are not the issue. It is the usage of decorators as a syntax that is the issue. Using decorators as functions works and preserve typing. Now, anyway, I'm convinced that mutation of classes is something that should not be encouraged. I even think that classes are a flawed pattern that Typescript doesn't require at all, so this whole class decorator pattern is like adding bad practice to a bad practice to me. I understand your points but they make me very sad. |
It's ok if you don't like something, but if you disregard an entire paradigm because folks abuse it, you'd not write a single line of code -- everything can be abused. It doesn't make the tools any less good when used properly. Now that decorators are approaching Stage 4, I would like to see the ability to have decorators mutate the type of a property. For example, using dependency injection: class Store {
query(){}
}
class Demo {
@service(Store) store;
// ^ has type of: Store
// (the instance of the class, Store)
} |
I ran into this today, and I don't understand how (TC39) decorators are technically supported in TypeScript 5? I don't mean that snarkily. I mean, if a decorator cannot output a type different from the input type, then it's technically not a TypeScript feature yet? Isn't this more of a bug in type inference from a decorator than a request? |
Yes, TC39 doesn't say anything about decorated class/methods' signatures and return values, only that the decorators must take and return functions. As long as the result of a decorator is a function, the spec is followed, but TS is (incorrectly) assuming the decorator output type is the same as the input, despite the TC39 proposal not saying anything like that. It doesn't even require the output to extend the input, let alone be the same as it. The inputs and outputs just have to extend Function. |
Adding my 2 cents here. I've read through a fair amount of comments here. I think this 8 years long tracked issue should be resolved. lets consider this code. interface B {
b: boolean;
}
function MakeItAlsoB<T extends { new (...args: any[]): any }>(Base: T) {
return class extends Base implements B {
public b: boolean;
constructor(...args: any[]) {
super(...args);
this.b = true;
}
};
}
@MakeItAlsoB
class A {
}
console.log(new A().b); // ts-will-error: Property 'b' does not exist on type 'A'.
const newA = MakeItAlsoB(A)
console.log(new newA().b); // no problem mate -> true
console.log((new A() as any).b); // no problem at runtime -> true The console logs at the end really explains all.
So
But since num.2 we know is correct because of TC39 I assume that either I'm missing something or TypeScript is not working as it should. Notice that the The @MakeItAlsoB
class A {
method() {
console.log(this.b); // ts-will-error: Property 'b' does not exist on type 'A'.
}
} For those still marketing that this could lead to confusion because of things like this. interface B {
methodOwner(): string;
}
function MakeItAlsoB<T extends { new (...args: any[]): any }>(Base: T) {
return class extends Base implements B {
methodOwner() {
return "b";
}
};
}
interface C {
methodOwner(): string;
}
function MakeItAlsoC<T extends { new (...args: any[]): any }>(Base: T) {
return class extends Base implements C {
methodOwner() {
return "c";
}
};
}
@MakeItAlsoC
@MakeItAlsoB
class A {
methodOwner() {
return "a";
}
}
console.log(new A().methodOwner()); This can cause confusion. and it's not immediately evident what the output should be. (its "c"). Then to those i would like to respond that this is not an issue in the requested feature "Decoration mutation" and also falls under the developer skill gap. |
10 years issue ! what happening Microsoft ? |
+1 |
@mhegazy The issue is open since almost 10 years, can you please re-evaluate its tags and therefore consider it to be included in a future typescript version ? otherwise, what is required to move forward ? |
From a practical standpoint, I've been wondering what this would like like to implement. Theoretically, decorators could be typed similarly to functions: pass the type of the annotated item into the decorator, and then use the decorators return type as the new type. As a comparison of decorators and functions and how they are typed: // ====== DECORATOR ======
function classDecorator<T extends {}>(
target: T,
context: ClassMethodDecoratorContext,
): T & { metadata: PropertyKey } {
return Object.assign(target, { metadata: context.name });
}
class TestClass {
@classDecorator
testMethod() {}
}
// @ts-expect-error metadata is not inferred from the decorator
console.info(new TestClass().testMethod.metadata);
// "testMethod"
// ====== FUNCTION ======
function functionDecorator<T extends {}>(
target: T,
context: { name: PropertyKey },
): T & { metadata: PropertyKey } {
return Object.assign(target, { metadata: context.name });
}
function testFunction() {}
const decoratedTestFunction = functionDecorator(testFunction, {
name: "testFunction",
});
console.info(decoratedTestFunction.metadata);
// "testFunction" See TS Playground here. Essentially, decorators only really care about input right now, and their output types are ignored (you can see this in the classDecorator example above: the return value of the decorator does not keep me from decorating the class method). Certain decorators are not allowed to modify values (see field decorators), but regardless, the return type of field decorators must match anyway, so I think it could be possible to calculate decorator types similarly to functions. I'm no TS internal expert, but it definitely seems like something that could be ported over since the functionality kinda already exists and just isn't being used! One caveat: if this was added, it could be initially confusing. A decorated method doesn't immediately scream "Possibly different type". Your eyes are drawn to the declaration type on the annotated item (method, accessor, etc.) and you may not immediately know that it's actually wrapped by a decorator that changes its return type. Not a huge thing, just may be a slight paradigm shift for TS programmers looking at decorated class members. |
@npenin well, we could also admit that, if it has been 10 years, maybe there is no issue to begin with, and thus nothing to improve. |
If we can get this to type check properly, we would have perfect support for boilerplate-free mixins:
The text was updated successfully, but these errors were encountered: