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

Consider this unsuccessful 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 restrict 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 , not Animal .

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

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

 function DecorateClass<T>(instantiate: (json:any) => T){ return (classTarget:typeof T) => { /*...*/ } // Cannot resolve symbol T } 
+5
source share
2 answers

Hold the line for just a second ...

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

In principle, using the new type, you can call the relationship between the class and its instance, which accurately and perfectly answers your question:

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

Description

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

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

This is similar to a new type (constructor function), which may or may not accept arguments and return any (instance). However, nothing says that it should be returned any - it can be a general type.

And since we have exactly what is returned from the constructor function (by type - the input class to which the decorator is applied) inside the type parameter, we can use this to determine the return type of the passed callback function.

I tested the decorator and it seems to work exactly 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 will actually invalidate my previous answer.


Edit: Inheritance

When inheritance is involved (for example: a derived type must be returned from instantiate ), assignability seems to be upside down: you can return a base type, but not a derived type.

This is because the return type from instantiate takes precedence over the "returned" classTarget type during a typical output type. The following question explores this specific issue:

+4
source

edit

It turns out that what you ask is quite possible. I have added a new answer, but I will leave this one here, since it may contain valuable information for someone. This answer offers a solution at runtime; the new one offers a solution at compile time.


I would say that it is best for you to check the type at runtime, since 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 lead to compile-time type security.

+2
source

Source: https://habr.com/ru/post/1242918/


All Articles