Skip to content
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

Open
Gaelan opened this issue Sep 20, 2015 · 265 comments
Open

Request: Class Decorator Mutation #4881

Gaelan opened this issue Sep 20, 2015 · 265 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@Gaelan
Copy link

Gaelan commented Sep 20, 2015

If we can get this to type check properly, we would have perfect support for boilerplate-free mixins:

declare function Blah<T>(target: T): T & {foo: number}

@Blah
class Foo {
    bar() {
        return this.foo; // Property 'foo' does not exist on type 'Foo'
    }
}

new Foo().foo; // Property 'foo' does not exist on type 'Foo'
@mhegazy mhegazy added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Sep 21, 2015
@g162h3
Copy link

g162h3 commented Sep 21, 2015

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 Promise<any>.

@red-0ne
Copy link

red-0ne commented Oct 28, 2015

@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'

@HerringtonDarkholme
Copy link
Contributor

+1 for this. Though I know this is hard to implement, and probably harder to reach an agreement on decorator mutation semantics.

@andreas-karlsson
Copy link

+1

@masaeedu
Copy link
Contributor

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.

@davojan
Copy link

davojan commented Mar 29, 2016

@masaeedu Do you know any workaround to add a static member to the decorated class?

@masaeedu
Copy link
Contributor

@davojan Sure. Here you go:

class A { }
namespace A {
    export let foo = function() { console.log("foo"); }
}
A.foo();

@nevir
Copy link

nevir commented Jun 4, 2016

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)

@joyt
Copy link

joyt commented Jun 21, 2016

The react-redux typings for connect takes a component and returns a modified component whose props don't include the connected props received through redux, but it seems TS doesn't recognize their connect definition as a decorator due to this issue. Does anyone have a workaround?

@JakeGinnivan
Copy link

I think the ClassDecorator type definition needs changing.

Currently it's declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;. Maybe it could be changed to

declare type MutatingClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction;
declare type ClassDecorator = MutatingClassDecorator | WrappingClassDecorator;

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).

@masaeedu
Copy link
Contributor

masaeedu commented Jun 28, 2016

@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.

@JakeGinnivan
Copy link

@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.

// React types
class Component<TProps> {
    props: TProps
}
class ComponentClass<TProps> {
}
interface ComponentDecorator<TOriginalProps, TOwnProps> {
(component: ComponentClass<TOriginalProps>): ComponentClass<TOwnProps>;
}

// Redux types
interface MapStateToProps<TStateProps, TOwnProps> {
    (state: any, ownProps?: TOwnProps): TStateProps;
}

// Fake react create class
function createClass(component: any, props: any): any {
}

// Connect wraps the decorated component, providing a bunch of the properies
// So we want to return a ComponentDecorator which exposes LESS than
// the original component
function connect<TStateProps, TOwnProps>(
    mapStateToProps: MapStateToProps<TStateProps, TOwnProps>
): ComponentDecorator<TStateProps, TOwnProps> {
    return (ComponentClass) => {
        let mappedState = mapStateToProps({
            bar: 'bar value'
        })
        class Wrapped {
            render() {
                return createClass(ComponentClass, mappedState)
            }
        }

        return Wrapped
    }
}


// App Types
interface AllProps {
    foo: string
    bar: string
}

interface OwnProps {
    bar: string
}

// This does not work...
// @connect<AllProps, OwnProps>(state => state.foo)
// export default class MyComponent extends Component<AllProps> {
// }

// This does
class MyComponent extends Component<AllProps> {
}
export default connect<AllProps, OwnProps>(state => state.foo)(MyComponent)
//The type exported should be ComponentClass<OwnProps>,
// currently the decorator means we have to export ComponentClass<AllProps>

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.

@blai
Copy link

blai commented Jun 28, 2016

According to https://github.com/wycats/javascript-decorators#class-declaration, my understanding is that the proposed declare type WrappingClassDecorator = <TFunction extends Function, TDecoratorFunction extends Function>(target: TFunction) => TDecoratorFunction; is invalid.

@blai
Copy link

blai commented Jun 28, 2016

The spec says:

@F("color")
@G
class Foo {
}

is translate to:

var Foo = (function () {
  class Foo {
  }

  Foo = F("color")(Foo = G(Foo) || Foo) || Foo;
  return Foo;
})();

So if I understand it correctly, the following should be true:

declare function F<T>(target: T): void;

@F
class Foo {}

let a: Foo = new Foo(); // valid
class X {}
declare function F<T>(target: T): X;

@F
class Foo {}

let a: X = new Foo(); // valid
let b: Foo = new Foo(); // INVALID
declare function F<T>(target: T): void;
declare function G<T>(target: T): void;

@F
@G
class Foo {}

let a: Foo = new Foo(); // valid
class X {}
declare function F<T>(target: T): void;
declare function G<T>(target: T): X;

@F
@G
class Foo {}

@G
class Bar {}

@F
class Baz {}

let a: Foo = new Foo(); // valid
let b: X = new Foo(); // INVALID
let c: X = new Bar(); // valid
let d: Bar = new Bar(); // INVALID
let e: Baz = new Baz(); // valid
class X {}
declare function F<T>(target: T): X;
declare function G<T>(target: T): void;

@F
@G
class Foo {}

@G
class Bar {}

@F
class Baz {}

let a: X = new Foo(); // valid
let b: Bar = new Bar(); // valid
let c: X = new Baz(); // valid
let d: Baz = new Baz(); // INVALID

@nevir
Copy link

nevir commented Jun 28, 2016

@blai

For your example:

class X {}
declare function F<T>(target: T): X;

@F
class Foo {}

let a: X = new Foo(); // valid
let b: Foo = new Foo(); // INVALID

I'm assuming you mean that F returns a class that conforms to X (and is not an instance of X)? E.g:

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 Foo that is in scope of those let statements has been mutated by the decorator. The original Foo is no longer reachable. It's effectively equivalent to:

let Foo = F(class Foo {});

@blai
Copy link

blai commented Jun 28, 2016

@nevir Yep, you are right. Thanks for clarification.

@blai
Copy link

blai commented Jun 28, 2016

On a side note, it seems like turning off the check to invalidate mutated class types is relatively easy:

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 06591a7..2320aff 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -11584,10 +11584,6 @@ namespace ts {
           */
         function getDiagnosticHeadMessageForDecoratorResolution(node: Decorator) {
             switch (node.parent.kind) {
-                case SyntaxKind.ClassDeclaration:
-                case SyntaxKind.ClassExpression:
-                    return Diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression;
-
                 case SyntaxKind.Parameter:
                     return Diagnostics.Unable_to_resolve_signature_of_parameter_decorator_when_called_as_an_expression;
         }

         /** Check a decorator */
        function checkDecorator(node: Decorator): void {
             const signature = getResolvedSignature(node);
             const returnType = getReturnTypeOfSignature(signature);
             if (returnType.flags & TypeFlags.Any) {
@@ -14295,9 +14291,7 @@ namespace ts {
             let errorInfo: DiagnosticMessageChain;
             switch (node.parent.kind) {
                 case SyntaxKind.ClassDeclaration:
-                    const classSymbol = getSymbolOfNode(node.parent);
-                    const classConstructorType = getTypeOfSymbol(classSymbol);
-                    expectedReturnType = getUnionType([classConstructorType, voidType]);
+                    expectedReturnType = returnType;
                     break;

                 case SyntaxKind.Parameter:
         }

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

// @target:es5
// @experimentaldecorators: true
class X {}
class Y {}

declare function dec1<T>(target: T): T | typeof X;
declare function dec2<T>(target: T): typeof Y;

@dec1
@dec2
export default class C {
}

var c1: X | Y = new C();
var c2: X = new C();
var c3: Y = new C();

It generates tests/baselines/local/decoratorOnClass10.types

=== tests/cases/conformance/decorators/class/decoratorOnClass10.ts ===
class X {}
>X : X

class Y {}
>Y : Y

declare function dec1<T>(target: T): T | typeof X;
>dec1 : <T>(target: T) => T | typeof X
>T : T
>target : T
>T : T
>T : T
>X : typeof X

declare function dec2<T>(target: T): typeof Y;
>dec2 : <T>(target: T) => typeof Y
>T : T
>target : T
>T : T
>Y : typeof Y

@dec1
>dec1 : <T>(target: T) => T | typeof X

@dec2
>dec2 : <T>(target: T) => typeof Y

export default class C {
>C : C
}

var c1: X | Y = new C();
>c1 : X | Y
>X : X
>Y : Y
>new C() : C
>C : typeof C

var c2: X = new C();
>c2 : X
>X : X
>new C() : C
>C : typeof C

var c3: Y = new C();
>c3 : Y
>Y : Y
>new C() : C
>C : typeof C

I was expecting
>C: typeof C to be >C: typeof X | typeof Y

@seansfkelley
Copy link

seansfkelley commented Jul 4, 2016

For those interested in react-redux's connect as a case study for this feature, I've filed DefinitelyTyped/DefinitelyTyped#9951 to track the issue in one place.

@esbanarango
Copy link

🙏

@minwe
Copy link

minwe commented Mar 2, 2023

+1

1 similar comment
@Merded
Copy link

Merded commented Mar 19, 2023

+1

@oleg-codaio
Copy link

I know this is an open issue, but I wanted to chime in given the revamped support for ES6 decorators in TypeScript 5.0.

Decorators can mutate classes by returning an updated value, and this works at runtime, but augmented types are essentially discarded. Here's a TS Playground link for what would ideally be supported one day.

@dzcpy
Copy link

dzcpy commented Jun 23, 2023

Really need this feature. Please add it in TypeScript 6 😂

@Marckon
Copy link

Marckon commented Jul 27, 2023

WE NEED THIS!!

@ericmorand
Copy link

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.

@syabro
Copy link

syabro commented Jul 28, 2023

@ericmorand please replace we with I.

@lolmaus
Copy link

lolmaus commented Jul 28, 2023

@ericmorand Strictly speaking, you don't need a computer in the first place. 😕

@ericmorand
Copy link

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.

@ericmorand
Copy link

@ericmorand please replace we with I.

Interesting that you didn't make the same answer to @Marckon when they wrote:

WE NEED THIS!!

;)

@MartinJohns
Copy link
Contributor

fact that an issue opened for 8 years

It's been waiting for decorators to officially be supported. That has only recently been the case (TypeScript 5.0).

that is natively available using functional programming since the beginning of the existence of TypeScript

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.

@freebeego
Copy link

freebeego commented Sep 6, 2023

+1

@cgauld
Copy link

cgauld commented Sep 8, 2023

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 @Component arguments is lost during compilation - it doesn't make it in to the .d.ts file for the file that the component is defined in. If decorators could mutate the type they return, then they could carry their arguments through into the type information (e.g. the .d.ts) by unioning them into the target type with a symbol key.

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).

@ericmorand
Copy link

@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
```

@saltman424
Copy link

@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\'')
}

Playground Link

@ericmorand
Copy link

ericmorand commented Sep 9, 2023

@saltman424

Especially since many developers don't understand what the decorators are doing, they just follow the documentation.

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.

@NullVoxPopuli
Copy link

NullVoxPopuli commented Oct 26, 2023

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)
}

@matthew-dean
Copy link

matthew-dean commented Nov 16, 2023

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?

@pjeby
Copy link

pjeby commented Nov 29, 2023

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.

@vekexasia
Copy link

vekexasia commented Feb 28, 2024

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.

  • At runtime we know we can access the b property.

So

  1. either this should be considered a bug => TypeScript cannot infer that A is indeed A & B
  2. or b should not exist at runtime

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 newA example is there just to show that TS is indeed fully capable to infer the proper type after the decoration.

The newA approach unfortunately is far from being perfect. Consider this

@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.

@jonlepage
Copy link

10 years issue ! what happening Microsoft ?

@dustin-rcg
Copy link

dustin-rcg commented Mar 2, 2024

Using decorators as functions to generate a new class has a considerable usability limitation: The resulting class type isn't available as a type in the normal way.

@Component
class MyComponent {
    bar: 'bar';
}

In this case I can immediately write:

const x: MyComponent = new MyComponent();

However, with this

// let's use the decorator as a function
export const MyOtherComponent = Component(MyComponent);

I can only write

const y: InstanceType<typeof MyOtherComponent> = new MyOtherComponent();

Surely you can see how awkward that is. We want decorated classes to work in parity with non-decorated ones.
It's also common to use decorators from multiple libraries on the same class. For example, using decorators for typeorm, type-graphql, and class-validator all on the same class is very convenient. Using these as functions would be a mess of nested function calls on the class.

The closest I have come so far -- given the order in which class member decorators execute before the class decorator and that class member decorators receive no information about the class 😞 -- is to instantiate a helper class that bundles all these decorators into a shared scope:

const meta = new EntityMeta<Employee>();

@meta.Entity()
class Employee {
  @meta.PrimaryColumn({ sqlType: "int" })
  id: number;

  @meta.Column({ sqlType: "text" })
  firstName: string;

  @meta.Column({ sqlType: "text" })
  lastName: string;

  @meta.Column({ sqlType: "text" })
  email: string;
}

However, the drawbacks here:

  • I have to explicitly state the class type in EntityMeta<Employee>. Surprisingly, although I have typed the Entity decorator as Constructor<T>, typescript doesn't catch when applied to a different type.
  • If I want to define multiple decorated classes in the same file, I need to have multiple variables with instances of EntityMeta, and typescript generally won't catch if I accidentally reuse the same instance in the wrong place.

See in the screenshot that the constructor for the EntityMeta instance is Constructor<Foo> but applied to Employee class and typescript doesn't catch it.
Screenshot from 2024-03-02 12-52-36

@dor6-deepcred
Copy link

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests