Extending type when extending an ES6 class with a TypeScript decorator
Asked Answered
C

3

17

I am trying to decorate a class with a decorator (a-la-angular style), and add methods and properties to it.

this is my example decorated class:

@decorator
class Person{

}

and this is the decorator:

const decorator = (target)=>{
    return class New_Class extends target {
        myProp:string
    }
}

but myProp is not a known property of Person:

person.myProp //Error - myProp does not exist on type Person

How can I decorate a typescript class and preserve type completion, type safety, etc ?

Crean answered 26/2, 2019 at 19:3 Comment(0)
C
5

To supplement jcalz response, going back to the definition of the Decorator Pattern, it does not change the interface/contracts of its target. It's not just terminology. TypeScript decorators share similarities with Java annotations and .NET attributes which are aligned with the fact not to change the interface: they just add metadata.

Class mixin is a good candidate to solve your question. But it's better not to use "decorator" in its name in order to avoid confusion.

Coats answered 27/2, 2019 at 10:52 Comment(0)
U
7

I found a solution to implement kind-of multiple heritage (really cascades it) but it's worth taking a look.

Supose you have a class Base with a few properties and methods:

class Base {
    tableName = 'My table name';
    hello(name) {
     return `hello ${name}`;
    }
}

And you want a class to extend Base but you also have defined some properties you want to reuse. For that will do the following function:

type Constructor<T = {}> = new (...args: any[]) => T;
function UserFields<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        name: string;
        email: string;
    };
}

Now we can do a class that extends Base and extends UserFields and typescript language service will find properties from both classes. It emulates a multiple heritage but it's really a cascade.

class User extends UserFields(Base) { }
const u = new User();
u.tableName = 'users'; // ok
u.name = 'John'; // ok

By this way you could reuse the UserFields function with any other classes. A clear example of that is if you want to expose the User object on client side as a "clean" object and have the fields available and then you have a UserDb object with connections to database and any other server side methods can have the same fields too. We only define the database fields once!

Another good thing is you could chain mixins mixin1(mixin2....(Base)) to have as many attributes as you want to have in the same class.

Everybody is expecting the decorator attributes to be visible by typescript, but meanwhile is a good solution.

Untimely answered 4/4, 2019 at 16:16 Comment(6)
I couldn't make the UserFields function work but would suggest the following: function UserFields<T extends new (...args: any[]) => {}>(constructor: T) { return class extends constructor { name: string; email: string; } }Personnel
It really looks like the declarations are equivalent, pretty good if it works anyway...Untimely
I'm not sure, type Constructor seems to be unused in your example? Typo perhaps?Personnel
Ohhh, yes, Typo for sure... it should be function UserFields<TBase extends Constructor>.... will edit the commentUntimely
It is worth noting that in some case it is important to copy static membersTape
This is called a mixin and this has nothing to do with decorators.Goatsucker
H
6

There's a GitHub issue about this with lots of discussion. I think the summary of it is: decorators do not mutate the type of a class (most of the discussion is about whether it should or shouldn't be that way) and therefore you can't do it the way you want, like this:

const decorator = (target: new (...args: any) => any) => {
  // note I'm extending target, not Person, otherwise you're not
  // decorating the passed-in thing
  return class New_Class extends target {
    myProp!: string
  }
}

@decorator
class Person {
  noKnowledgeOfMyProp: this['myProp'] = "oops"; // error
}

declare const p: Person;
p.myProp; // error, uh oh

What you could do instead is just use your decorator as a plain mixin function, and have Person extend the return value of that. You end up having two class definitions... one which is passed into decorator and one which your new class extends. The "inner" class (passed to decorator()) still has no knowledge of the added props, but the "outer" class does:

class Person extends decorator(class {
  innerProp: string = "inner";
  noKnowledgeOfMyProp: this['myProp'] = "oops"; // error
}) {
  outerProp: string = "outer"
  hasKnowledgeOrMyProp: this['myProp'] = "okay"; // okay
}

declare const p: Person;
p.myProp; // okay

Does that help? Good luck!

Heterogenous answered 26/2, 2019 at 19:13 Comment(1)
thank you for your reply @jcalz. I'm trying to build a framework that enables its users to simply add an "@entity" decorator to their model, so your solutions syntax not so user friendly. Right now I have a partial solution, I instruct my users to extend an "Entity" abstract class, which has all the properties and methods declared. I was hoping to find a more friendly solution but I guess it's not currently possible..Crean
C
5

To supplement jcalz response, going back to the definition of the Decorator Pattern, it does not change the interface/contracts of its target. It's not just terminology. TypeScript decorators share similarities with Java annotations and .NET attributes which are aligned with the fact not to change the interface: they just add metadata.

Class mixin is a good candidate to solve your question. But it's better not to use "decorator" in its name in order to avoid confusion.

Coats answered 27/2, 2019 at 10:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.