Búsqueda de sitios web

typescript - Usando decoradores de clase, ¿puedo obtener el tipo de la instancia del tipo de clase?


Considera este ejemplo fallido:

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;
}

Aquí quiero restringir que la función anónima en el decorador siempre devuelva una instancia de la clase en cuestión, pero lo anterior no funciona ya que T es en realidad typeof Animal y no Animal.

En una función genérica, ¿hay alguna manera de obtener el tipo Animal a partir del tipo typeof Animal sin ser molestamente verboso como definiendo explícitamente todos los tipos como function DecorateClass<TTypeOfClass, TClass>(...)?

Desafortunadamente, el uso de typeof en la sintaxis genérica no es compatible, lo cual era mi mejor opción al intentar que el compilador entendiera lo que quiero:

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

Respuesta #1

Mantén la línea solo por un segundo...

Recientemente he necesitado una definición de tipo para una función que recibe una clase como argumento y devuelve una instancia de esa clase. Cuando encontré una solución, esta pregunta pronto vino a mi mente.

Básicamente, usando un tipo renovable es posible conjurar una relación entre una clase y su instancia, lo cual responde a tu pregunta de manera precisa y perfecta:

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

Explicación

En TypeScript, cualquier tipo instanciable se puede definir con la siguiente firma:

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

Esto es análogo a un tipo renovable (la función constructora) que puede o no tomar argumentos y devuelve any (la instancia). Sin embargo, nada dice que deba ser any lo que se devuelva: también puede ser un tipo genérico.

Y dado que tenemos exactamente lo que se devuelve desde la función constructora (al inferir el tipo de la clase a la que se aplica el decorador) dentro de un parámetro de tipo genérico, podemos usar eso para definir el tipo de retorno de la función de callback pasada.

He probado el decorador, y parece estar funcionando exactamente como se esperaba:

@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;
}

Esto invalida efectivamente mi respuesta anterior.

Editar: Herencia

Cuando se trata de herencia (por ejemplo: se debe devolver un tipo derivado desde instantiate), la asignabilidad parece invertirse: se puede devolver un tipo base, pero no un tipo derivado.

Esto se debe a que el tipo devuelto por instantiate tiene prioridad sobre el tipo "devuelto" de classTarget durante la inferencia de tipos genéricos. La siguiente pregunta examina este mismo problema:

  • Prioridad de inferencia de parámetros de tipo genérico en TypeScript

Respuesta #2

Editar

Resulta que lo que estás pidiendo es totalmente posible. He añadido una nueva respuesta pero dejaré esta aquí también, ya que podría contener información valiosa para alguien. Esta respuesta sugiere una solución en tiempo de ejecución, la nueva sugiere una solución en tiempo de compilación.

Diría que tu mejor opción es la verificación de tipos en tiempo de ejecución, ya que tendrás el tipo correcto dentro de la función decoradora:

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

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

        // ...
    }
}

Esto no producirá seguridad de tipo en tiempo de compilación.

Artículos relacionados