microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

Home Page:https://www.typescriptlang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Request: Class Decorator Mutation

Gaelan opened this issue · comments

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'

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

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

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.

commented

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

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

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

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

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.

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

@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 {});

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

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

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.

I've read all comments on this issue and got an idea that decorator's signature doesn't actually shows what it can do with wrapped class.

Consider this one:

function decorator(target) {
    target.prototype.someNewMethod = function() { ... };
    return new Wrapper(target);
}

It should be typed in that way:
declare function decorator<T>(target: T): Wrapper<T>;

But this signature doesn't tell us that decorator has added new things to the target's prototype.

On the other hand, this one doesn't tell us that decorator has actually returned a wrapper:
declare function decorator<T>(target: T): T & { someMethod: () => void };

Any news on this? This would be super powerful for metaprogramming!

What about a simpler approach to this problem? For a decorated class, we bind the class name to the decorator return value, as a syntactic sugar.

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

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

// is desugared to
const Foo = Blah(class Foo {
  // this.foo is not available here
})

new Foo.foo // foo is available here.

Implementation-wise, this will introduce one synthetic symbol for decorated class. And the original class name is only bound to class body scope.

@HerringtonDarkholme I think that would be a nicely pragmatic approach that would provide most of the expressiveness desired. Great Idea!

I definitely want to see this someday

I often write a class for Angular 2 or for Aurelia, that looks like this:

import {Http} from 'aurelia-fetch-client';
import {User} from 'models';

// accesses backend routes for 'api/user'
@autoinject export default class UserService {
  constructor(readonly http : Http) { }
  
  readonly resourceUrl = 'api/users';
  
  async get(id: number) {
    const response = await this.http.fetch(this.resourceUrl);
    const user = await response.json() as User;
    return user;
  }

  async post(id: number, model: { [K in keyof User]?: User[K] }) {
    const response = await this.http.post(`${this.resourceUrl}/`${id}`, model);
    return await response.json();
  }
}

What I want to write is something like
decorators/api-client.ts

import {Http} from 'aurelia-fetch-client';

export type Target = { name; new (...args): { http: Http }};

export default function apiClient<T extends { id: string }>(resourceUrl: string) {
  return (target: Target)  => {
    type AugmentedTarget = Target & { get(id: number): Promise<T>, post(id, model: Partial<T>) };
    const t = target as AugmentedTarget;
    t.prototype.get = async function (id: number) {
      const response = await this.http.fetch(resourceUrl);
      return await response.json() as T;
    }
  }
}

and then I could generically apply it like

import {Http} from 'aurelia-fetch-client';
import apiClient from ./decorators/api-client
import {User} from 'models';

@apiClient<User>('api/users') export default class UserService {
  constructor(readonly http : Http) { }
}

with no loss of typesafety. This would be a boon for writing clean, expressive code.

Reviving this issue.

Now that #13743 is out and mixin support is in the language this is a super useful feature.

@HerringtonDarkholme is less suitable for this case though, having to declare the return type of the decorator looses some dynamic features...

@ahejlsberg, @mhegazy Do you think this is doable?

commented

I have another usage scenario I'm not sure is yet covered by this conversation but probably falls under the same umbrella.

I would like to implement a method decorator that changes the type of the method entirely (not the return type or parameters but the entire function). e.g.

type AsyncTask<Method extends Function> = {
    isRunning(): boolean;
} & Method;

// Decorator definition...
function asyncTask(target, methodName, descriptor) {
    ...
}

class Order {
    @asyncTask
    async save(): Promise<void> {
        // Performs an async task and returns a promise
        ...
    }
}

const order = new Order();

order.save();
order.save.isRunning(); // Returns true

Totally possible in JavaScript, that's not the problem obviously, but in TypeScript I need the asyncTask decorator to change the type of the decorated method from () => Promise<void> to AsyncTask<() => Promise<void>>.

Pretty sure this isn't possible now and probably falls under the umbrella of this issue?

@codeandcats your example is the exact same use case I am here for!

commented

Hi @ohjames, forgive me, I'm having trouble groking your example, any chance you could rewrite into something that works as is in the playground?

Any progress on this? I had this in my head all day, unaware of this issue, went to go implement it only to find out that the compiler doesn't pick up on it. I have a project that could use a better logging solution so I wrote a quick singleton to later expand into a full-fledged logger that I was going to attach to classes via a decorator like

@loggable
class Foo { }

and I wrote the necessary code for it

type Loggable<T> = T & { logger: Logger };

function loggable<T extends Function>(target: T): Loggable<T>
{
	Object.defineProperty(target.prototype, 'logger',
		{ value: Logger.instance() });
	return <Loggable<T>> target;
}

and the logger property is definitely present at runtime but regrettably not picked up by the compiler.

I would love to see some resolution to this issue, especially since a runtime construct like this should absolutely be able to be properly represented at compile-time.

I ended up settling for a property decorator just to get me by for now:

function logger<T>(target: T, key: string): void
{
	Object.defineProperty(target, 'logger',
		{ value: Logger.instance() });
}

and attaching it to classes like

class Foo {
    @logger private logger: Logger;
    ...

but this is far more boilerplate per class utilizing the logger than a simple @loggable class decorator. I suppose I could feasibly typecast like (this as Loggable<this>).logger but this is also pretty far from ideal, especially after doing it a handful of times. It'd get tiresome very quickly.

I had to can TS for an entire app mainly cause I was unable to get https://github.com/jeffijoe/mobx-task working with decorators. I hope this will be addressed soon. 😄

It's very irritating in the Angular 2 ecosystem where decorators and TypeScript are treated as first class citizens. Yet the minute you try to add a property with a decorator the TypeScript compiler says no. I would have thought the Angular 2 team would show some interest in this issue.

@zajrik you can accomplish what you want with class mixins that have been supported with proper typing since TS 2.2:

Define your Loggable mixin like so:

type Constructor<T> = new(...args: any[]) => T;

interface Logger {}

// You don't strictly need this interface, type inference will determine the shape of Loggable,
// you only need it if you want to refer to Loggable in a type position.
interface Loggable {
  logger: Logger;
}

function Loggable<T extends Constructor<object>>(superclass: T) {
  return class extends superclass {
    logger: Logger;
  };
}

and then you can use it in a few ways. Either in the extends clause of a class declaration:

class Foo {
  superProperty: string;
}

class LoggableFoo extends Loggable(Foo) {
  subProperty: number;
}

TS knows that instances of LoggableFoo have superProperty, logger, and subProperty:

const o = new LoggableFoo();
o.superProperty; // string
o.logger; // Logger
o.subProperty; // number

You can also use a mixin as an expression that returns the concrete class you want to use:

const LoggableFoo = Loggable(Foo);

You can also use a class mixin as a decorator, but it has some slightly different semantics, mainly that is subclasses your class, rather than allowing your class to subclass it.

Class mixins have several advantages over decorators, IMO:

  1. They create a new superclass, so that the class you apply them to has a change to override them
  2. They type check now, without any additional features from TypeScript
  3. They work well with type inference - you don't have to type the return value of the mixin function
  4. They work well with static analysis, especially jump-to-definition - Jumping to the implementation of logger takes you to the mixin implementation, not the interface.

@justinfagnani I hadn't even considered mixins for this so thank you. I'll go ahead and write up a Loggable mixin tonight to make my Logger attachment syntax a bit nicer. The extends Mixin(SuperClass) route is my preferred as it's how I've used mixins so far since the release of TS 2.2.

I do prefer the idea of decorator syntax to mixins, however, so I do still hope some resolution can be had for this specific issue. Being able to create boilerplate-free mixins using decorators would be a huge boon to cleaner code, in my opinion.

@zajrik glad the suggestion helped, I hope

I still don't quite understand how mixins have more boilerplate than decorators. They're nearly identical in syntactic weight:

Class Mixin:

class LoggableFoo extends Loggable(Foo) {}

vs Decorator:

@Loggable
class LoggableFoo extends Foo {}

In my opinion, the mixin is way more clear about its intention: it's generating a superclass, and superclasses define members of a class, so the mixin is probably defining members as well.

Decorators will be used for so many things that you can't assume that is or isn't defining members. It could simply be registering the class for something, or associating some metadata with it.

commented

To be fair I think what @zajrik wants is:

@loggable
class Foo { }

Which is undeniably, if ever so slightly, less boilerplate.

That said, I love the mixin solution. I keep forgetting that mixins are a thing.

If all you care about is adding properties to the current class then mixins are basically equivalent to decorators with one significant annoyance... if your class doesn't already have a superclass you need to create an empty superclass to use them. Also the syntax seems heavier in general. Also it isn't clear if parametric mixins are supported (is extends Mixin(Class, { ... }) allowed).

@justinfagnani in your list of reasons, points 2-4 are actually deficiencies in TypeScript not advantages of mixins. They don't apply in a JS world.

I think we should all be clear that a mixin based solution to OPs problem would involve adding two classes to the prototype chain, one of which is useless. This reflects the semantic differences of mixins Vs decorators though, mixins give you a chance to intercept the parent class chain. However 95% of the time this isn't what people want to do, they want to decorate this class. Whilst mixins have their limited uses i think promoting them as an alternative to decorators and higher order classes is semantically inappropriate.

Mixins are basically equivalent to decorators with one significant annoyance... if your class doesn't already have a superclass you need to create an empty superclass to use them

This isn't necessarily true:

function Mixin(superclass = Object) { ... }

class Foo extends Mixin() {}

Also the syntax seems heavier in general.

I just don't see how this is so, so we'll have to disagree.

Also it isn't clear if parametric mixins are supported (is extends Mixin(Class, { ... }) allowed).

They very much are. Mixins are just functions.

in your list of reasons, points 2-4 are actually deficiencies in TypeScript not advantages of mixins. They don't apply in a JS world.

This is a TypeScript issue, so they apply here. In the JS world decorators don't actually exist yet.

I think we should all be clear that a mixin based solution to OPs problem would involve adding two classes to the prototype chain, one of which is useless.

I'm not clear where you get two. It's one, just like the decorator might do, unless it's patching. And which prototype is useless? The mixin application presumably adds a property/method, that's not useless.

This reflects the semantic differences of mixins Vs decorators though, mixins give you a chance to intercept the parent class chain. However 95% of the time this isn't what people want to do, they want to decorate this class.

I'm no so sure this is true. Usually when defining a class you expect it to be at the bottom of the inheritance hierarchy, with the ability to override superclass methods. Decorators either have to patch the class, which has numerous problems, including not working with super(), or extend it, in which case the decorated class does not have an ability to override the extension. This can be useful in some cases, like a decorators that overrides every defined method of the class for performance/debugging tracing, but it's far from the usual inheritance model.

Whilst mixins have their limited uses i think promoting them as an alternative to decorators and higher order classes is semantically inappropriate.

When a developer wants to add members to the prototype chain, mixins are exactly semantically appropriate. In every case that I've seen someone want to use decorators for mixins, using class mixins would accomplish the same task, with the semantics that they're actually expecting out of decorators, more flexibility due to working property with super calls, and of course they work now.

Mixins are hardly inappropriate when they directly address the use case.

When a developer wants to add members to the prototype chain

That's exactly my point, the OP doesn't want to add anything to the prototype chain. He just wants to mutate a single class, and mostly when people use decorators they don't even have a parent class other than Object. And for some reason Mixin(Object) doesn't work in TypeScript so you have to add a dummy empty class. So now you have a prototype chain of 2 (not including Object) when you don't need it. Plus there is a non-trivial cost to adding new classes to the prototype chain.

As for the syntax compare Mixin1(Mixin2(Mixin3(Object, { ... }), {... }), {...}). The parameters for each mixin are as far from the mixin-class as they could be. Decorator syntax is clearly more readable.

While decorator syntax per-se doesn't type check, you can just use regular function invocation to get what you want:

class Logger { static instance() { return new Logger(); } }
type Loggable<T> = T & { logger: Logger };
function loggable<T, U>(target: { new (): T } & U): { new (): Loggable<T> } & U
{
    // ...
}

const Foo = loggable(class {
    x: string
});

let foo = new Foo();
foo.logger; // Logger
foo.x; // string

It's just a little annoying that you have to declare your class as const Foo = loggable(class {, but aside from that it all works.

@ohjames (cc @justinfagnani) you have to be careful when extending builtins such as Object (since they bash over your subclass's prototype in instances): https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work

@nevir yep, I already tried @justinfagnani's suggestion of using a mixin with a default Object parameter in the past with TypeScript 2.2 and tsc rejects the code.

@ohjames it still works, you just have to be careful about the prototype in the default case (see that FAQ entry).

Though, it's generally easier to rely on tslib.__extend's behavior when passed null

Any plans to focus this issue in the next iteration step? The benefits of this feature is extreme high accross so much libraries.

I just ran into this issue - it forces me to write a lot of unneeded code. Resolving this issue would be a huge help to any decorator-based framework/library.

@TomMarius As I've mentioned earlier, classes wrapped in decorator functions already type check properly, you just can't use the @ syntax sugar. Instead of doing:

@loggable
class Foo { }

you just need to do:

const Foo = loggable(class { });

You can even compose a bunch of decorator functions together before wrapping a class in them. While making the syntax sugar work properly is valuable, it doesn't seem like this should be such a huge pain point as things are.

@masaeedu Really the issue is not external but internal type support. Being able to use the properties the decorator adds within the class itself without compilation errors is the desired result, at least for me. The example you've provided would only give Foo the loggable type but would not afford the type to the class definition itself.

@zajrik A decorator returns a new class from an original class, even when you use the built in @ syntax. Obviously JS doesn't enforce purity, so you're free to mutate the original class you are passed, but this is incongruent with idiomatic use of the decorator concept. If you're tightly coupling functionality that you're adding via decorators to the class internals, they may as well be internal properties.

Can you give me an example of a use case for a class internally consuming API that is added at some later point via decorators?

The Logger example above is a good example of a common want for being able to manipulate the internals of the decorated class. (And is familiar to people coming from other languages with class decoration; such as Python)

That said, @justinfagnani's class mixin suggestion seems like a good alternative for that case

If you want to be able to define the internals of a class, the structured way to do this isn't to patch the class, or define a new subclass, both which TypeScript will have a hard time reasoning about in the context of the class itself, but to either just define things in the class itself, or create a new superclass that has the needed properties, which TypeScript can reason about.

Decorators really shouldn't change the shape of a class in a way that's visible to the class or most consumers. @masaeedu is right on here.

While what you're saying is true, TypeScript isn't there to enforce clean coding practices, but to correctly type JavaScript code, and it fails in this case.

@masaeedu What @zajrik said. I have a decorator that declares an "online service" that adds bunch of properties to a class that are then used in the class. Subclassing or implementing an interface isn't an option because of missing metadata and constraint enforcement (if you strive for no code duplication).

@TomMarius My point is that it is type checking correctly. When you apply a decorator function to a class, the class is not changed in any way. A new class is produced via some transformation of the original class, and only this new class is guaranteed to support the API introduced by the decorator function.

I don't know what "missing metadata and constraint enforcement" means (perhaps a concrete example would help), but if your class explicitly relies on the API introduced by the decorator, it should just subclass it directly via the mixin pattern @justinfagnani showed, or inject it through the constructor or something. The utility of decorators is that they allow classes that are closed to modification to be extended for the benefit of code that consumes those classes. If you're at liberty to define the class yourself, just use extends.

@masaeedu If you're developing some kind of, let's say, a RPC library, and you want to force the user to write asynchronous methods only, inheritance-based approach forces you to duplicate code (or I haven't found the correct way, maybe - I'd be glad if you told me if you know how).

Inheritance-based approach
Definition: export abstract class Service<T extends { [P in keyof T]: () => Promise<IResult>}> { protected someMethod(): Promise<void> { return Promise.reject(""); } }
Usage: export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } }

Decorator-based approach
Definition: export function service<T>(target: { new (): T & { [P in keyof T]: () => Promise<IResult> } }) { target.someMethod = function () { return Promise.reject(""); }; return target; }
Usage: @service export default class { async foo() { return this.someMethod(); } }

You can clearly see the code duplication in the inheritance-based approach example. It has happened many times to me and my users that they've forgot to change the type parameter when they copy-pasted the class or started to use "any" as the type parameter and the library stopped working for them; the decorator-based approach is much more developer friendly.

After that there's yet another problem with the inheritance-based approach: reflection metadata are missing now, so you have to duplicate code even more because you have to introduce the service decorator anyways. The usage is now: @service export default class MyService extends Service<MyService> { async foo() { return this.someMethod(); } }, and that's just plain unfriendly, not just a little inconvience.

You're true that semantically the modification is done after the class is defined, however, there's no way to instantiate the undecorated class, so there's no reason not to properly support mutation of the class other than that it sometimes allows unclean code (but sometimes for the better good). Remember that JavaScript is still based on prototypes, the class syntax is just a sugar to cover it. The prototype is mutable and might be muted by the decorator, and it should be correctly typed.

When you apply a decorator function to a class, the class is not changed in any way.

Not true, when you apply a decorator function to a class, the class may be changed, in any way. Whether you like it or not.

@TomMarius You're trying to exploit inference to enforce some contract, which is totally irrelevant to the argument being had here. You should just do:

function service<T>(target: { new (): T & {[P in keyof T]: () => Promise<any> } }) { return target };

// Does not type check
const MyWrongService = service(class {
	foo() { return ""; }
})

// Type checks
const MyRightService = service(class {
	async foo() { return ""; }
})

There is absolutely no requirement for the internals of the class to be aware of the decorating function.

@masaeedu That wasn't my point. The service decorator/Service class introduces some new properties, and these are always available to the class to be used, but the type system doesn't correctly reflect that. You said I should use inheritance for that, so I've shown you why I can't/don't want to.

I've edited the example to be more clear.

@masaeedu By the way, the statement "A decorator returns a new class from an original class" is incorrect - every decorator we both have shown here returns a mutated or directly the original class, never a new one.

@TomMarius Your comment mentioned a problem with "forcing the user to write asynchronous methods only", which is the problem I tried to address in my comment. Enforcing that the user has followed the contract you expect should be done wherever the user's code is passed back to the library, and doesn't have anything to do with the discussion about whether decorators should change the type-shape presented to the class internals. The orthogonal problem of providing an API to the user's code can be solved with standard inheritance or composition approaches.

@ohjames The class is not changed merely by applying a decorator. JS doesn't enforce purity, so obviously any statement anywhere in your code can modify anything else, but this is irrelevant to this feature discussion. Even once the feature is implemented, TypeScript won't be helping you track arbitrary structural changes within function bodies.

@masaeedu You're picking bits, but I'm talking about the big picture. Please review all my comments in this thread - the point is not in the individual issues but in each issue happening at the same time. I think I explained the problem with inheritance-based approach well - lots and lots of code duplication.

For clarity, this isn't about "clean code" to me. The problem is one of practicality; you don't need massive changes to the type system if you treat @foo the same as function application. If you open up the can of worms of trying to introduce type information to a function's argument from its return type, while at the same time interacting with type inference and all the other magical beasts found in various corners of the TypeScript type system, I feel this will become a become a big obstacle for new features in much the same way as overloading is now.

@TomMarius Your first comment in this thread is about clean code, which is not relevant. The next comment is about this online service concept that you provided the example code for. The primary complaint, all the way from the first paragraph to the fourth, is about how it is error prone to use MyService extends Service<MyService>. I tried to show an example of how you can deal with this.

I've looked at it again, and I really can't see anything in that example that illustrates why the members of the decorated class would need to be aware of the decorator. What is it about these new properties that you provide to the user that cannot be accomplished with standard inheritance? I haven't worked with reflection, so I kind of skimmed over that, my apologies.

@masaeedu I can accomplish it with inheritance, but inheritance forces me/my users to massively duplicate code, so I'd like to have another way - and I do, but the type system isn't able to correctly reflect the reality.

The point is that correct type of @service class X { } where service is declared as <T>(target: T) => T & IService isn't X, but X & IService; and the problem is that in reality it's true even inside the class - even though it semantically isn't true.

Another huge pain point this issue causes is that when you have a series of decorators each having some constraints, the type system thinks that the target is always the original class, not the decorated one, and thus the constraint is useless.

I can accomplish it with inheritance, but inheritance forces me/my users to massively duplicate code, so I'd like to have another way.

This is the part I'm not understanding. Your users need to implement IService, and you want to ensure TheirService implements IService also obeys some other contract { [P in keyof blablablah] }, and perhaps you also want them to have a { potato: Potato } on their service. All of this is easy to accomplish without the class members being aware of @service:

import { serviceDecorator, BaseService } from 'library';

// Right now
const MyService = serviceDecorator(class extends BaseService {
    async foo(): { return ""; }
})

const MyBrokenService1 = serviceDecorator(class extends BaseService {
    foo(): { return; } // Whoops, forgot async! Not assignable
});

const MyBrokenService2 = serviceDecorator(class { // Whoops, forgot to extend BaseService! Not assignable
    async foo(): { return; } 
});

// Once #4881 lands
@serviceDecorator
class MyService extends BaseService {
    async foo(): { return ""; }
}

In neither case is there some giant win in succinctness that can only be achieved by presenting the return type of serviceDecorator as the type of this to foo. More importantly, how do you propose to come up with an even sound-ish typing strategy for this? The return type of serviceDecorator is inferred based on the type of the class you're decorating, which in turn is now typed as the return type of the decorator ...

Hi @masaeedu, the succinctness becomes especially valuable when you have more than one.

@Component({ /** component args **/})
@Authorized({/** roles **/)
@HasUndoContext
class MyComponent  {
  // do stuff with undo context, component methods etc
}

This workaround however is only an alternative to class decorators. For method decorators there is currently no solutions and blocks so much nice implementations. Decorators are in proposal in stage 2 - https://github.com/tc39/proposal-decorators . However there were lot of cases the implementation was made lot earlier. I think especially decorators are one of those important bricks which were really important since they were used already in so much frameworks and a very simple version is already implemented in babel/ts. If this issue could be implemented it wouldn't lose it's experimental state until the official release. But that's "experimental" for.

@pietschy Yes, making the @ syntax sugar work properly with type checking would be nice. At the moment you can use function composition to get reasonably similar succinctitude:

const decorator = compose(
    Component({ /** component args **/ }),
    Authorized({ /** roles **/ })
    HasUndoContext
);

const MyComponent = decorator(class {
});

The preceding discussion is about whether it is a good idea to do some kind of inverse typing where the return type of the decorator is presented as the type of this to the class members.

Hi @masaeedu, yep I understood the context of the discussion, hence the // do stuff with undo context, component methods etc. Cheers

commented

really need make Mixins easier.

typescript (javascript) do not support multiple inheritance, so we must use Mixins or Traits.
And now It's wasting me so many time, especially when I reconstruct something.
And I must copy & paste an interface with its "empty implementation" everywhere. (#371)

--
I do not think the return type of decorator should presented on the class.
because: .... emmm. not know how to describe it, sorry for my poor english skill ... ( 🤔 can a photo exists without a photo frame ? ) this job is for an interface.

Gonna add my +1 to this one! I'd love to see it coming soon.

@pietschy If the class is dependent on members added by the decorators, then it should be extending the result of the decorators, not the other way around. You should do:

const decorator = compose(
    Component({ /** component args **/ }),
    Authorized({ /** roles **/ })
    HasUndoContext
);

class MyComponent extends decorator(class { }) {
    // do stuff with undo context, component methods etc
};

The alternative is for the type system to work in some kind of loop, where the decorator function's argument is contextually typed by its return type, which is inferred from its argument, which is contextually typed by its return type, etc. We still need a specific proposal for how this should work, it isn't just waiting on implementation.

Hi @masaeedu, I'm confused as to why I'd have to compose my decorators and apply that to a base class. As far as I understand it the prototype has already been modified by the time any user land code executes. E.g.

function pressable<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        pressMe() {
            console.log('how depressing');
        }
    }
}

@pressable
class UserLandClass {
    constructor() {    
        this['pressMe'](); // this method exists, please let me use code completion.
    }
}

console.log(new UserLandClass());

So if methods defined by decorators already exist and can legitimately called then it would be nice if typescript reflected this.

The desire is for typescript to reflect this without imposing workarounds. If there are other
things that decorators can do that can't be modelled then it would at least be nice if this
scenario was supported in some shape or form.

This thread is full of opinions on what decorators should do, how they should be used, how they shouldn't be used, etc ad nauseam.

This is what decorators actually do:

function addMethod(Class) : any {
    return class extends Class {
        hello(){}
    };
}

@addMethod
class Foo{
    originalMethod(){}
}

which becomes, if you're targeting esnext

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

function addMethod(Class) {
    return class extends Class {
        hello() {
        }
    };
}
let Foo = class Foo {
    originalMethod() { }
};
Foo = __decorate([
    addMethod
], Foo);

with __decorate applying each decorator from bottom to top, with each one having the option to return a whole new class.

I understand getting the type system to support and acknowledge this may very well be tricky, and I understand it may very well take time to support this, but can we not all agree that the current behavior, from the original code sample above that started this thread, is simply incorrect?

No matter what any developer's opinion is on decorators or mixins or functional composition etc etc etc, this appears to be a pretty clear bug.


Can I politely inquire as to whether this happens to be in the pipeline for a future release?

TypeScript is simply amazing and I love it; this however seems like one of the few (only?) pieces that's simply broken, and I'm just looking forward to seeing it fixed :)

@arackaf It is well understood what decorators actually do, and assuming you don't care about the types TypeScript's emit already supports decorators. The entire debate in this issue is about how decorated classes should be presented in the type system.

I agree that new Foo().foo being a type error is a bug, and an easily fixed one at that. I do not agree that return this.foo; being a type error is a bug. If anything, you are asking for a feature in the type system that so far no one in this issue has yet specified. If you have some mechanism in mind by which the type of this should be transformed by a decorator applied to the containing class, you need to suggest this mechanism explicitly.

Such a mechanism is not as trivial as you might expect, because in nearly all cases the return type of the decorator is inferred from the very type you are proposing to transform using the return type of the decorator. If decorator addMethod takes a class of type new () => T and produces new() => T & { hello(): void }, it doesn't make sense to suggest that T should then be T & { hello(): void }. Within the decorator body, is it valid for me to refer to super.hello?

This is especially pertinent because I'm not restricted to doing return class extends ClassIWasPassed { ... } in the body of the decorator, I can do whatever I please; subtraction types, mapped types, unions, they're all fair game. Any resultant type must play well with this inference loop you're suggesting. As an illustration of the problem, please tell me what should happen in this example:

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello()) // Do I get a type error here? After all, the signature of Class is { hello: () => void }
        } 
    };
}

// Or should I get the error here? Foo does not conform to { hello(): string }
@logger
class Foo {
    hello() { return "foo"; }
}

You can't just handwave the problem away as being "tricky", someone needs to actually propose how this should work.

Not an error - you're passing Logger a class which conforms to Greeter; however Foo is then mutated by the decorator to be something completely different, which no longer extends Greeter. So it should be valid for the logger decorator to treat Foo as Greeter, but invalid for anyone else to, since Foo is reassigned to something completely different by the decorator.

I'm sure implementing this would be visciously difficult. Is some reasonable subset possible, for the common cases like the one listed at the top of this thread, with workarounds like interface merging being fallbacks for tricky edge cases like this?

@arackaf

however Foo is then mutated by the decorator to be something completely different

There is no precise definition for what Foo is in the first place. Remember that our entire point of contention is whether Foo's members should be able to access (and hence return as part of the public API), decorator introduced members from this. What Foo is is defined by what the decorator returns, and what the decorator returns is defined by what Foo is. They're inseparably spliced.

I'm sure implementing this would be viciously difficult

It is still premature to talk about implementation complexity, when we don't even have a solid proposal for how the feature should work. Apologies for harping on this, but we really need someone to propose a concrete mechanism for "what happens to this when I apply such a sequence of decorators to such a class". We can then plug in various TypeScript concepts in different parts and see whether what comes out makes sense. Only then does it make sense to discuss implementation complexity.

Is the current decorator output that I pasted above not spec compliant? I assumed it was from what I've seen.

Assuming it is, Foo has a fairly precise meaning every step of the way. I would expect TS to allow me to use this (Inside of Foo methods and on instances of Foo) based on what the last decorator returns, with TS forcing me to add type annotations along the way as needed if the decorator functions aren't sufficiently analyzable.

@arackaf There are no "steps of the way"; we're only concerned with the final type of this for a given immutable code snippet. You need to describe, at least at a high level, what you expect the type of this should be for members of a class X decorated with decorator functions f1...fn, in terms of the type signatures of X and f1...fn. You can have as many steps in the process as you like. So far, no one has been doing this. I've been guessing that people mean the return type of the decorator should be presented as the type of this, but for all I know I could be totally off the mark.

If your proposal is to mechanically make the types reflect what is happening to the values in the transpiled output, you end up with what I'm proposing instead of what you're proposing: i.e. new Foo().hello() is fine, but this.hello() is not. At no point in that example does the original class you are decorating gain a hello method. Only the result of __decorate([addMethod], Foo) (which is then assigned to Foo) has a hello method.

I've been guessing that people mean the return type of the decorator should be presented as the type of this

Oh, sorry, yes, that's right. Exactly that. Full stop. Because that's what the decorators DO. If the last decorator in line returns some completely new class, then that IS what Foo is.

In other words:

@c
@b
@a
class Foo { 
}

Foo is whatever in the world c says it is. If c is a function that returns any then, I don't know - maybe just fallback to the original Foo? That seems like a reasonable, backward compatible approach.

but if c returns some new type X, then I absolutely would expect Foo to respect that.

Am I missing something?


clarifying further, if

class X { 
    hello() {}
    world() {}
}
function c(cl : any) : X {  // or should it be typeof X ?????
    //...
}

@c
@b
@a
class Foo { 
    sorry() {}
}

new Foo().hello(); //perfectly valid
new Foo().sorry(); //ERROR 

Am I missing something?

@arackaf Yes, what this naive approach is missing is that a decorator is free to return arbitrary types, with no restriction that the result be compatible with the type of Foo.

There's any number of absurdities you can produce with this. Let's say I suspend my objection about the circularity of typing this as the result of c, which is determined by the type of this, which is determined by the result of c, etc. This still doesn't work. Here's another nonsensical example:

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello())
        }
    };
}


@logger
class Foo {
    foo() { return "bar" }
    // Whoops, `this` is `{ hello(): void }`, it has no `foo` method
    hello() { return this.foo(); }
}

For this case, you're either forced to produce an error for totally benign code, or adjust the proposition that this be exactly identical to the return type of the decorator.

I'm not sure I see the problem. @logger has chosen to return a brand new class, without any foo method, and with a totally new hello() method, which happens to reference back to the original, now-unreachable Foo.

new Foo().foo()

is no longer valid; it will produce a runtime error. I'm just saying it should also produce a compile-time error.

That said, if statically analyzing all of that is too hard, it would be totally reasonable imo to force us to add an explicit type annotations to logger to signify exactly what is being returned. And if no such type annotation is present, then I'd say just revert to assuming Foo is returned back. That should keep it backward compatible.

@arackaf There is no problem with the code in terms of typing or runtime evaluation. I can call new Foo().hello(), which will internally call the decorated class's hello, which will call the decorated class's bar. It is not an error for me to invoke bar inside the original class.

You can try it out for yourself by running this full example in the playground:

// Code from previous snippet...

const LoggerFoo = logger(Foo)
new LoggerFoo().hello()

Sure - but I said it was an error to invoke

new Foo().foo()

There is no problem with the code in terms of typing or runtime evaluation. I can call new Foo().hello(), which will internally call the decorated class's hello, which will call the decorated class's bar

But it should be an error to say

let s : string = new Foo().hello();

since Foo's hello method now returns void, per the class that Logger returns.

Sure - but I said it was an error to invoke new Foo().foo()

@arackaf But that doesn't matter. I didn't invoke new Foo().foo(). I invoked this.foo(), and I got a type error, even though my code works fine at runtime.

But it should be an error to say let s : string = new Foo().hello()

Again, this is irrelevant. I am not saying the final type of Foo.prototype.hello should be () => string (I agree it should be () => void). I am complaining about the valid invocation this.bar() erroring out, because you've surgically transplanted a type where it is nonsensical to transplant it.

There are two Foo's here. When you say

class Foo { 
}

Foo is an immutable binding WITHIN the class, and a mutable binding outside the class. So this works perfectly as you can verify in a jsbin

class Foo { 
  static sMethod(){
    alert('works');
  }
  hello(){ 
    Foo.sMethod();
  }
}

let F = Foo;

Foo = null;

new F().hello();

Your example above does similar things; it captures a reference to the original class before that outer binding is mutated. I'm still not sure what you're driving at.

this.foo(); is perfectly valid, and I wouldn't expect a type error (I also wouldn't blame the TS people if I needed an any reference, since I'm sure tracing this through would be hard)

this.foo(); is perfectly valid, and I wouldn't expect a type error

Good, so we agree, but this means you now have to qualify or dismiss the proposal that this be the instance type of whatever the decorator returns. If you think it shouldn't be a type error, what should this be instead of { hello(): void } in my example?

this depends on what was instantiated.

@c
class Foo{
}

new Foo(). // <---- this is based on whatever c returned 

function c(Cl){
    new Cl().  // <----- this is an object whose prototype is the original Foo's prototype
                   // but for TS's purpose, for type errors, it'd depend on how Cl is typed
}

Could we please stick to one concrete example? That would make things much easier for me to understand. In the following snippet:

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello())
        }
    };
}

@logger
class Foo {
    foo() { return "bar" }
    hello() { return this.foo(); } /// <------
}

What is the type of this? If it is { hello(): void }, then I will get a type error, because foo is not a member of { hello(): void }. If it is not { hello(): void }, then this is not simply the instance type of the decorator's return type, and you need to explain whatever alternate logic you're using to arrive at the type of this.

EDIT: Forgot to add the decorator on Foo. Fixed.

this, where you have the arrows, is of course an instance of the original Foo. There's no type error.

Ah - I see your point now; but I still don't see where the problem is. this.foo() WITHIN the original Foo is not a type error - that is valid for the (now unreachable) class that used to be bound to the identifier Foo.

It's an idiosyncrasy, fun trivia, but I don't see why this should prevent TS from handling mutating class decorators.

@arackaf You're not answering the question. What, specifically, is the type of this? You can't just endlessly circularly answer "this is Foo and Foo is this". What members does this have? If it has any members besides hello(): void, what logic is being used to determine them?

When you say "this.foo() WITHIN the original Foo is not a type error", you still need to answer the question: what is the structural type of this such that it is not a type error to do this.foo()?

Also, the original class is not "unreachable". Every function defined in that code snippet is actively exercised at runtime, and it all works without a hitch. Please run the playground link I provided and look at the console. The decorator returns a new class in which the hello method delegates to the hello method of the decorated class, which in turn delegates to the foo method of the decorated class.

It's an idiosyncrasy, fun trivia, but I don't see why this should prevent TS from handling mutating class decorators.

There's no "trivia" in the type system. You're not going to get a TSC-1234 "naughty boy, you can't do that" type error because the use case is too niche. If a feature is causing perfectly normal code to break in surprising ways, the feature needs to be rethought.

You're not going to get a TSC-1234 "naughty boy, you can't do that" type error because the use case is too niche.

That's EXACTLY what I get when I try to use a method that was added to a class definition by a decorator. I currently have to work around it by either adding a definition to the class, or using interface merging, casting as any etc.

I've answered every question about what this is, and where.

The simple fact of the matter is that the meaning of this changes based on where you are.

type Constructor<T> = new (...args: any[]) => T
type Greeter = { hello(): string }

function logger<T extends Constructor<Greeter>>(Class: T) {
    return class {
        hello() {
            console.log(new Class().hello())
        }
    };
}

class Foo {
    foo() { return "bar" }
    hello() { return this.foo(); } /// <------
}

const LoggableFoo = logger(Foo)
new LoggableFoo().hello() // Logs "bar"

when you say new Class() - Class is pointing to the original Foo, and from TypeScript's perspective it'd have access to hello(): string since that's what Class is typed as (extends Greeter). At runtime you'll be instantiating an instance of the original Foo.

new LoggableFoo().hello() happens to call a void method that happens to call a method that's otherwise unreachable, typed through Greeter.

If you'd done

@logger
class Foo {
    foo() { return "bar" }
    hello() { return this.foo(); }
}

then Foo is now a class with only hello(): void and new Foo().foo() should be a Type error.

And, again, hello() { return this.foo(); } is not a TypeError - why would it be? Just because that class definition is no longer reachable doesn't somehow make that method invalid.

I don't expect TypeScript to get any of these edge cases perfect; it'd be pretty understandable to have to add type annotations here and there, like always. But none of these examples show why @logger can't fundamentally change what Foo is bound to.

If logger is a function which returns a new class, then THAT is what Foo now references.

I've answered every question about what this is, and where.
The simple fact of the matter is that the meaning of this changes based on where you are.

This is really frustrating. Fine, it changes, it is Foo, it is a static binding, etc. etc. etc. What is the type signature? What members does this have? You're talking about everything under the sun, when all I need from you is a simple type signature for what this is inside hello.

new LoggableFoo().hello() happens to call a void method that happens to call a method that's otherwise unreachable, typed through Greeter.

This is not the same as unreachable. Every reachable method is "otherwise unreachable" when you discount the paths through which it is reachable.

If you'd done:

But this is literally what I have done. That is exactly my code snippet, pasted again, prefaced with an explanation of the example I constructed for you. I even ran it through a diff checker to make sure I wasn't taking crazy pills, and the only difference is the comment you removed.

And, again, hello() { return this.foo(); } is not a TypeError - why would it be?

Because you (and others here) want the type of this to be the instantiated return type of the decorator, which is { hello(): void } (notice the absence of a foo member). If you want the members of Foo to be able to see this as the return type of the decorator, the type of this inside hello will be { hello(): void }. If it is { hello(): void }, I get a type error. If I get a type error, I am sad, because my code runs fine.

If you say it is not a type error, you are abandoning your own scheme for supplying the type of this via the return type of the decorator. The type of this is then { hello(): string; bar(): string }, regardless of what the decorator returns. You may have some alternative scheme for producing the type of this that avoids this problem, but you need to specify what it is.

You seem to not understand that, after the decorators run, Foo can reference something totally separate from what it was originally defined as.

function a(C){
    return class {
        x(){}
        y(){}
        z(){}
    }
}

@a
class Foo {
    a(){ 
        this.b();  //valid
    }
    b() { 
        this.c();  //also valid
    }
    c(){ 
        return 0;
    }
}

let f = new Foo();
f.x(); //valid
f.y(); //also valid
f.z(); //still valid

I surmise you find some strangeness in this being something different inside of Foo above, than it is in instances that are subsequently created from what Foo eventually is (after the decorators run)?

I'm not really sure what to tell you; that's just how decorators work. My only argument is that TypeScript should more closely match what's happening.

To put it another way, the type signatures inside the (original) Foo will be different than what Foo is/produces once the decorators have run.

To borrow an analogy from another language, inside the decorated Foo this would be referencing the equivalent of a C# anonymous type—a totally real type, that's otherwise valid, just not really able to be referenced directly. It would look weird getting the errors and non-errors described above, but again, that's just how it works. Decorators give us tremendous power to do bizarre things like this.

I surmise you find some strangeness in this being something different inside of Foo above, than it is in instances that are subsequently created from what Foo eventually is (after the decorators run)?

No. I don't find any strangeness in that, because it is exactly what I was proposing 200 comments ago. Did you even read any of the preceding discussion?

The snippet you have posted is totally uncontroversial. The people I was disagreeing with, and whose aid you jumped to additionally want the following:

function a(C){
    return class {
        x(){}
        y(){}
        z(){}
    }
}

@a
class Foo {
    a(){ 
        this.b();  //valid
    }
    b() { 
        this.c();  //also valid
    }
    c(){ 
        return 0;
    }
    d(){
        // HERE: All of these are also expected to be valid
        this.x();
        this.y();
        this.z();
    }
}

let f = new Foo();
f.x(); //valid
f.y(); //also valid
f.z(); //still valid

I disagree that it is possible to do this soundly, and have desperately been trying to figure out how such a suggestion would work. Despite my best efforts, I can't extract a comprehensive list of the members this is expected to have, or how such this list would be constructed under the proposal.

To put it another way, the type signatures inside the (original) Foo will be different than what Foo is/produces once the decorators have run.

So why are you even arguing with me? I said: "I agree that new Foo().foo being a type error is a bug, and an easily fixed one at that. I do not agree that return this.foo; being a type error is a bug". Analogously, in your example, I agree that new Foo().x() being a type error is a bug, but this.x() being a type error is not.

You see how there are two comments in the snippet at the top of this page?

        return this.foo; // Property 'foo' does not exist on type 'Foo'

^ That one is the one I find problematic. Either you agree that the decorator's return type shouldn't be presented on this, and only on new Foo(), in which case we're both arguing about nothing. Or you don't agree and want that feature as well, in which case the snippet in your previous comment is irrelevant.

I finally understand your point. It was extremely hard for me to get this from your Greeter code, but I'm tracking now; thank you for being so patient.

I'd say the only sensible solution would be for Foo (inside of Foo) to support the type union - derp I meant intersection - of the original Foo, and whatever the last decorator returns. You have to support the original Foo for crazy examples like your Greeter, and you definitely need to support whatever the last decorator returns since that's the whole point of using decorators (per the many comments above).

So yeah, from my most recent example, inside of Foo x, y, z, a, b, c would all work. If there were two versions of a, then support both.

@arackaf np, thanks for your patience as well. My examples haven't been the clearest because I'm just posting whatever I can cook up in playground that demonstrates how it's broken. It's hard for me to think about it systematically.

I'd say the only sensible solution would be for Foo (inside of Foo) to support the type union of the original Foo, and whatever the last decorator returns.

Ok awesome, so we're getting into the specifics of it now. Correct me if I'm getting this wrong, but when you say "union" you mean it should have the type members from both, i.e. it should be A & B. So we want this to be typeof(new OriginalClass()) & typeof(new (decorators(OriginalClass))), where decorators is the composed type signature of all the decorators . In plain English, we want this to be the intersection of the instantiated type of the "original class" and the instantiated type of the "original class" passed through all the decorators.

There's two problems with this. One is that in cases like my example, this just allows you to access non-existent members. I could add a bunch of members in the decorator, but if I tried accessing them in my class using this.newMethod(), it would just puke at runtime. newMethod is only added to the class returned from the decorator function, the original class's members have no access to it (unless I specifically happen to use the return class extends OriginalClass { newMethod() { } } pattern).

The other problem is that "original class" isn't a well defined concept. If I can access decorated members from this, I can also use them as part of the return statements, and hence they may be part of the "original class"'s public API. I'm kind of handwaving here, and I'm a bit too burnt out to think of concrete examples, but I think if we thought hard about it we could come up with nonsensical examples. Maybe you could work around this by finding a some way to segregate members that don't return things they accessed from this or at least for which the return type is not inferred as a result of returning this.something().

@masaeedu yeah, I corrected myself on the union / intersection thing prior to your reply. It's counter-intuitive for someone new to TS.

Understood on the rest. Honestly decorators won't usually return a completely different type, they'll usually just augment the type that was passed in, so the intersection thing will "just work" safely most of the time.

I'd say the runtime errors you talk about would be rare, and the result of some purposefully bad development decisions. I'm not sure you should really need to care about this but, if it truly is a problem, I'd say just using what the last decorator returned would be a decent second place (so yeah, a class could see a TypeError by trying to use a method that itself defines - not ideal, but still a worthwhile price to pay for decorators working).

But really, I think the runtime errors you're thinking of are not worth preventing, certainly at the expense of decorators working correctly. Besides, it's quite easy to produce runtime errors in TS if you're careless or silly.

interface C { a(); }
class C {
    foo() {
        this.a();  //<--- boom
    }
}

let c = new C();
c.foo();

Regarding your second objection

I can also use them as part of the return statements, and hence they may be part of the "original class"'s public API

I'm afraid I don't see any problems with that. I want anything added to the class via a decorator to absolutely be a first-class citizen. I'm curious what the potential problems there would be.