Pesquisa de site

node.js - TypeScript: Decorando uma classe derivada com função decoradora tipada


Estou tentando construir uma biblioteca ORM semelhante ao "Entity Framework" para node.js e MongoDB com TypeScript.

Com esta biblioteca, o consumidor poderá definir uma classe Model (ex. Person) que irá estender uma classe Base, e o decorador de classe adicionará dados adicionais à classe Person (por exemplo, o array instances, que conterá todas as instâncias do Model, e collection_name que será o nome da coleção do MongoDB para o modelo, etc.).

Código no playground de TypeScript.

Então meu primeiro passo foi criar uma classe Base:

class Base<T>
{
    static collection_name: string
    static instances: Base<any>[]
    _id: string
}

Então o usuário poderá definir seu modelo assim:

@decorator<Person>({collection_name: 'people'})
class Person extends Base<Person>
{
    @field name
    @field address
    ...
}

então eu criei uma classe decoradora para definir as propriedades collection_name e instances na classe Person:

function decorator<T>(config: { collection_name: string }) {
    return function <T extends Base<T>>(Class: IClass<T>) {
        Class.collection_name = config.collection_name;
        Class.instances = [];
        return Class
    }
}

a função decoradora recebe a Class gerada pelo usuário, e eu estou tentando criar uma interface que descreva o tipo de tal classe. Eu a chamei de IClass:

interface IClass<T>
{
    new(): Base<T>

    instances: Base<T>[];
    collection_name: string
}
  • new é o construtor (que retorna uma instância de Base)
  • instances e collection_name são propriedades estáticas de Base<T> e são não estáticas aqui (não tenho certeza sobre isso, isso está certo?)

No entanto, ao tentar definir o modelo de usuário, recebo o seguinte erro:

@decorator<Person>({collection_name: 'people'})   // <==== ERROR HERE ===
class Person extends Base<Person>
{
}

Erro:(23, 2) TS2345: Argumento do tipo 'typeof Person' não pode ser atribuído ao parâmetro do tipo 'IClass>'.

A propriedade 'instances' está ausente no tipo 'typeof Person', mas o tipo 'typeof Person' está ausente das seguintes propriedades do tipo exigido em 'IClass>'.

Parece que o compilador TypeScript está ignorando os membros estáticos herdados de Base ao verificar o tipo de typeof Person.

Como posso definir o tipo da propriedade da Classe da função decoradora?

Resposta #1

O problema, como jcalz aponta, é que seu decorator está aceitando uma Class de um tipo que já possui as propriedades estáticas instances e collection_name. Você precisa usar duas interfaces diferentes, uma que seja um tipo que simplesmente construa instâncias de T com a assinatura new(): T, e outra que estenda essa interface para incluir as propriedades estáticas que seu decorator irá adicionar.

class Base<T> {
    static _id = 0;
    private _id: number;

    constructor () {
        this._id = Base._id++;
    }
}

interface BaseConstructor<T extends Base<T>> {
    _id: number;
    new(): T;
}

interface DecoratedBaseConstructor<T extends Base<T>> extends BaseConstructor<T> {
    instances: T[];
    collection_name: string;
}

function decorator<T extends Base<T>>(config: { collection_name: string }) {
    return (baseConstructor: BaseConstructor<T>): DecoratedBaseConstructor<T> => {
        const decoratedBaseConstructor = baseConstructor as Partial<DecoratedBaseConstructor<T>>;
        decoratedBaseConstructor.collection_name = config.collection_name;
        decoratedBaseConstructor.instances = [];
        return decoratedBaseConstructor as DecoratedBaseConstructor<T>;
    };
}

@decorator<Person>({collection_name: 'people'})
class Person extends Base<Person> {
    name: string;

    constructor () {
        super();
        this.name = 'foo';
    }
}

Com essa abordagem, todos os membros static de Base devem ser públicos. Qualquer membro static de Base inicializado no decorator deve ir para o DecoratedBaseConstructor, e quaisquer membros static restantes que não forem inicializados no decorator devem ir para o BaseConstructor em vez disso.

Eu suponho que você use o tipo genérico T na classe Base de alguma forma no seu código atual, mas se você não usar, você deve remover o tipo genérico da classe Base e todo o resto ainda funcionará da mesma forma.

Confira o trecho acima neste playground.