3

I am trying to implement a class decorator in Typescript. So I have a function that takes a class as an argument.

const decorate = function ()
{
  return function ( target: any )
  {
    return class myExtendClass extends target{}
  };
};

The goal is to use it like:

@decorate()
class Something{}

Unfortunately I am getting type any is not a constructor function type. Any idea how I can achieve what I want?

Avraam Mavridis
  • 8,698
  • 19
  • 79
  • 133
  • maybe try changing `any` to `Function`? Check "Class Decorator" in http://stackoverflow.com/questions/29775830/how-to-implement-a-typescript-decorator, and https://blogs.msdn.microsoft.com/typescript/2015/04/30/announcing-typescript-1-5-beta/ – Sayan Pal Oct 09 '16 at 09:53
  • @SayanPal already tried with Function, not working. I find it a bit weird to have to use the Reflect API, since this is a valid js code it should be a valid ts code :) – Avraam Mavridis Oct 09 '16 at 10:06
  • You are trying to use a decorator to extend classes in your example, is that part of your main problem here? Or is your question just about getting a decorator to work? – Alex Oct 09 '16 at 11:53
  • @Alex extend a class inside a decorator. A decorator without extend works. I can "decorate" the class without using decorator, but my question is why this code doesnt work, since its valid js. – Avraam Mavridis Oct 09 '16 at 12:15

3 Answers3

5

my question is why this code doesnt work, since its valid js

That depends on what you mean by doesn't work.

The generated javascript is valid and will work. But TypeScript is much stricter and will complain if you write code that does not comply with the rules of TS, even though it often will play nice and give you the compiled js anyway.

The problem with what you are trying to do is that TS does not have support for extending classes via decorators. It will produce js that works, but TS does not understand what is happening when you do this.

For example:

const decorate = () => (target: typeof Cat) =>
{
    return class Lion extends target
    {
        public Roar = () => console.log("Roaar")
    }
}

@decorate()
class Cat
{
    public Purr = () => console.log("Purr purr");
}

var lion = new Cat();
lion.Roar(); // Error in TypeScript but working in js

This works when you run it, but TS does not understand what you are doing within the decorator, so it don't know that the class Cat actually is a Lion.

There is a feature request about making TS understand class mutations in decorators, but at this time it's not supported.

I suggest that you avoid mutating classes in decorators like this. And even if it would work, I don't see the point in your example as it is now, it's not very intuitive and if you want all Something to have certain properties you could just extend it directly: Something extends MyReusableProperties

Alex
  • 14,104
  • 11
  • 54
  • 77
  • 1
    Ofcourse I can achieve it with other ways, I cant provide the whole code on why a decorator is more elegant, it doesnt matter. Thx for your answer. – Avraam Mavridis Oct 09 '16 at 18:06
  • Ok, it would be interesting to see your real use-case. It would be nice for mixins for example. – Alex Oct 09 '16 at 18:44
2

You could try:

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

function decorate()
{
  return function (target: Newable<Object>)
  {
    return class myExtendClass extends target {
      public someOtherProp: string;
      publicconstructor() { 
        this.someOtherProp = "test2";
      }    
    }
  };
};

But then you will get a new error:

// Error: Type 'typeof myExtendClass' is 
// not assignable to type 'typeof Something'.
@decorate() 
class Something{
  public someProp: string;
  publicconstructor() { 
    this.someProp = "test";
  }
}

You can try to fix it:

function decorate()
{
  return function (target: Newable<Object>)
  {
    class myExtendClass extends target {
      public someOtherProp: string;
      publicconstructor() { 
        this.someOtherProp = "test2";
      }
    }
    return (<any>myExtendClass); // notice casting!
  };
};

And again you will get another error:

let something = new Something();
console.log(something.someProp);

// Property 'someOtherProp' does not 
//exist on type 'Something'.
console.log(something.someOtherProp);

Finally, you could solve that by casting to any:

console.log((<any>something).someOtherProp); // More casting!

Of course this is not an ideal solution. I would try do do something like the following instead:

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

function decorate()
{
  return function (target: Newable<Object>)
  {
    class myExtendClass extends target {
      public someOtherProp: string;
      publicconstructor() { 
        this.someOtherProp = "test2";
      }
    }
    return (<any>myExtendClass);
  };
};

function applyDecorator<T,TDecorated>(decorator, ctr): Newable<TDecorated> {
  // Decorators are just functions, you don't need @ symbol to use them :)
  let decorated: Newable<TDecorated> = <any>decorator()(ctr);
}

interface ISomething {
  someProp: string;
}

interface ISomethingElse extends Something {
  someOtherProp: string;
}

class Something implements ISomething {
  public someProp: string;
  publicconstructor() { 
    this.someProp = "test";
  }
}

let SomethingElse = applyDecorator<ISomething, ISomethingElse>(decorate, Something);

let something = new SomethingElse();
console.log(something.someProp);
console.log(something.someOtherProp);

Hope it helps :)

Remo H. Jansen
  • 23,172
  • 11
  • 70
  • 93
  • 1
    thx!. I will leave it open for some days before accept the answer, maybe someone comes up with a more elegant solution (I doubt). Thx anyway :) – Avraam Mavridis Oct 09 '16 at 10:09
0

I think the problem is with the signature of your decorator function. Change your function signature from function ( target: any ) into function <T extends { new (...args: any[]): {} }> . To put it simple in order the statement class myExtendClass extends target to work without error target has to be a class aka constructor function as it called in Javascript. You can't extend to any but to a class (constructor function). Here generic type T with constraint is constraining target to be a a class (constructor function). Therefore extend statement works without an error. Here is the example :

const decorate = function ()
{
    return function <T extends { new (...args: any[]): {} }>
        (target: T)
  {
      return class myExtendClass extends target{
          anotherProperty = 10; //assigning class property here
          propertyFromDecorator = "new property from decorator" // new property
    }
  };
};


@decorate()
class Something{
    myProp: string;
    anotherProperty:number;
    constructor() {
        this.myProp = "my property";
    }
} 

var someThing = new Something();
console.log(someThing);

Fore more please see Typescript Decorators page on github

Namig Hajiyev
  • 1,117
  • 15
  • 16