Reputation: 14503
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
Reputation: 31934
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
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 }) => { /*...*/ }
}
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.
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