Alex
Alex

Reputation: 14503

Using class decorators, can I get the type of the class-type instance?

Consider this failing example:

function DecorateClass<T>(instantiate: (...params:any[]) => T){
    return (classTarget:T) => { /*...*/ }
}

@DecorateClass((json:any) => {
    //Purely example logic here, the point is that it have to return
    //an instance of the class that the decorator runs on.
    var instance = new Animal();
    instance.Name = json.name;
    instance.Sound = json.sound;
    return instance;
})
class Animal {
    public Name:string;
    public Sound:string;
}

Here I want to constrain the anonymous function in the decorator to always return an instance of the class in question, but the above does not work since T is actually typeof Animal and not Animal.

In a generic function, is there anyway I can get type Animal from the type typeof Animal without being annoyingly verbose like explicitly defining all types like function DecorateClass<TTypeOfClass, TClass>(...)?

Unfortunately, using typeof in the generic syntax is not supported, which was my best bet in trying to get the compiler to understand what I want:

function DecorateClass<T>(instantiate: (json:any) => T){
    return (classTarget:typeof T) => { /*...*/  } // Cannot resolve symbol T
}

Upvotes: 7

Views: 3246

Answers (2)

John Weisz
John Weisz

Reputation: 31934

Edit

It turns out what you are asking for is entirely possible. I've added a new answer but will leave this one here as well, as it might contain information valuable to someone. This answer suggests a runtime solution, the new one suggests a compile-time solution.


I'd say your best bet is runtime type checking, as you will have the correct type inside the decorator function:

function DecorateClass(instantiate: (...params: any[]) => any) {
    return (classTarget: Function) => {
        var instance = instantiate(/*...*/);

        if (!(instance instanceof classTarget)) {
            throw new TypeError();
        }

        // ...
    }
}

This will not yield compile-time type safety.

Upvotes: 2

John Weisz
John Weisz

Reputation: 31934

Hold the line just for a second...

Recently I've needed a type definition for a function that takes in a class as an argument, and returns an instance of that class. When I came up with a solution, this question soon came to my mind.

Basically, using a newable type it is possible to conjure a relation between a class and its instance, which accurately and perfectly answers your question:

function DecorateClass<T>(instantiate: (...args: any[]) => T) {
    return (classTarget: { new(...args: any[]): T }) => { /*...*/ }
}

Explanation

In TypeScript, any given newable type can be defined with the following signature:

new(...args: any[]): any

This is analogous to a newable type (the constructor function) that may or may not take arguments and returns any (the instance). However, nothing says it must be any that is returned -- it can be a generic type as well.

And since we have exactly what is returned from the constructor function (by type-inferring the class the decorator is applied to) inside a generic type parameter we can use that to define the return type of the passed in callback function.

I've tested the decorator, and it seems to be working precisely as expected:

@DecorateClass((json: any) => {
    return new Animal(); // OK
})
@DecorateClass((json: any) => {
    return Animal; // Error
})
@DecorateClass((json: any) => {
    return "animal"; // Error
})
class Animal {
    public Name: string;
    public Sound: string;
}

This effectively invalidates my previous answer.


Edit: Inheritance

When inheritance is involved (eg.: a derived type is to be returned from instantiate), assignability seems to be flipped: you can return a base type, but not a derived type.

This is because the returned type from instantiate takes precedence over the "returned" type of classTarget during generic type-inference. The following question examines this exact problem:

Upvotes: 6

Related Questions