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

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:

  • Generic type parameter inference priority in TypeScript

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.

Tags:

Typescript