Junto com as hierarquias OO tradicionais, outra maneira popular de construir classes a partir de componentes reutilizáveis é construí-los combinando classes parciais mais simples. Você pode estar familiarizado com a ideia de mixins ou traits para linguagens como Scala, e o padrão também alcançou alguma popularidade na comunidade JavaScript.
Como funciona um Mixin?
O padrão depende do uso de Genéricos com herança de classe para estender uma classe base. O melhor suporte mixin do TypeScript é feito por meio do padrão de expressão de classe. Você pode ler mais sobre como esse padrão funciona em JavaScript aqui.
Para começar, precisaremos de uma classe que terá o mixin aplicado em cima de:
tsTry
classSprite {nome = "";x = 0;y = 0;constructor(nome : string) {this.nome =nome ;}}
Então você precisa de um tipo e uma função de fábrica que retorne uma expressão de classe estendendo a classe base.
tsTry
// Para começar, precisamos de um tipo que usaremos para estender// outras classes de. A principal responsabilidade é declarar// que o tipo que está sendo passado é uma classe.typeConstrutor = new (...args : any[]) => {};// Este mixin adiciona uma propriedade de escala, com getters e setters// para alterá-lo com uma propriedade privada encapsulada:functionEscala <TBase extendsConstrutor >(Base :TBase ) {return classDimensionamento extendsBase {// Mixins não podem declarar propriedades privadas/protegidas// entretanto, você pode usar campos privados ES2020_escala = 1;setEscala (escala : number) {this._escala =escala ;}getescala (): number {return this._escala ;}};}
Com tudo isso configurado, você pode criar uma classe que representa a classe base com mixins aplicados:
tsTry
classSprite {nome = "";x = 0;y = 0;constructor(nome : string) {this.nome =nome ;}}typeConstrutor = new (...args : any[]) => {};functionEscala <TBase extendsConstrutor >(Base :TBase ) {return classDimensionamento extendsBase {// Mixins não podem declarar propriedades privadas/protegidas// entretanto, você pode usar campos privados ES2020_escala = 1;setEscala (escala : number) {this._escala =escala ;}getescala (): number {return this._escala ;}};}// ---cortar---// Componha uma nova classe da classe Sprite,// com o aplicador Mixin Escala:constSpriteOitoBits =Escala (Sprite );constabanoSprite = newSpriteOitoBits ("Passaro");abanoSprite .setEscala (0.8);console .log (abanoSprite .escala );
Mixins restritos
Na forma acima, o mixin não tem nenhum conhecimento básico da classe, o que pode dificultar a criação do design que você deseja.
Para modelar isso, modificamos o tipo de construtor original para aceitar um argumento genérico.
tsTry
// Este era nosso construtor anterior:typeConstrutor = new (...args : any[]) => {};// Agora usamos uma versão genérica que pode aplicar uma restrição em// a classe a qual este mixin é aplicadotypeGConstrutor <T = {}> = new (...args : any[]) =>T ;
Isso permite a criação de classes que funcionam apenas com classes de base restritas:
tsTry
typeGConstrutor <T = {}> = new (...args : any[]) =>T ;classSprite {nome = "";x = 0;y = 0;constructor(nome : string) {this.nome =nome ;}}// ---corte---typePosicionavel =GConstrutor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstrutor <typeofSprite >;typeLoggable =GConstrutor <{impressao : () => void }>;
Então você pode criar mixins que só funcionam quando você tem uma base particular para construir:
tsTry
typeGConstrutor <T = {}> = new (...args : any[]) =>T ;classSprite {nome = "";x = 0;y = 0;constructor(nome : string) {this.nome =nome ;}}typePosicionavel =GConstrutor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstrutor <typeofSprite >;typeLoggable =GConstrutor <{impressao : () => void }>;// ---cortar---functionSaltavel <TBase extendsPosicionavel >(Base :TBase ) {return classSaltavel extendsBase {saltar () {// Este mixin só funcionará se for passado uma base// classe que tem setPos definido por causa da// Restrição posicionável.this.setPos (0, 20);}};}
Padrão Alternativo
As versões anteriores deste documento recomendavam uma maneira de escrever mixins em que você criava o tempo de execução e as hierarquias de tipo separadamente e depois os mesclava no final:
tsTry
// Cada mixin é uma classe ES tradicionalclassSaltavel {saltar () {}}classAbaixavel {abaixar () {}}// Incluindo a baseclassSprite {x = 0;y = 0;}// Então você cria uma interface que mescla// os mixins esperados com o mesmo nome de sua baseinterfaceSprite extendsSaltavel ,Abaixavel {}// Aplique os mixins na classe base via// JS em tempo de execuçãoaplicarMixins (Sprite , [Saltavel ,Abaixavel ]);letjogador = newSprite ();jogador .saltar ();console .log (jogador .x ,jogador .y );// Isso pode estar em qualquer lugar em sua base de código:functionaplicarMixins (derivadoCtor : any,construtores : any[]) {construtores .forEach ((baseCtor ) => {Object .getOwnPropertyNames (baseCtor .prototype ).forEach ((nome ) => {Object .defineProperty (derivadoCtor .prototype ,nome ,Object .getOwnPropertyDescriptor (baseCtor .prototype ,nome ) ||Object .create (null));});});}
Esse padrão depende menos do compilador e mais da sua base de código para garantir que o tempo de execução e o sistema de tipos sejam mantidos corretamente em sincronia.
Restrições
O padrão mixin é suportado nativamente dentro do compilador TypeScript por análise de fluxo de código. Existem alguns casos em que você pode atingir as bordas do suporte nativo.
Decoradores e Mixins #4881
Você não pode usar decoradores para fornecer mixins por meio de análise de fluxo de código:
tsTry
// Uma função decoradora que replica o padrão mixin:constPausavel = (alvo : typeofJogador ) => {return classPausavel extendsalvo {deveCongelar = false;};};@Pausavel classJogador {x = 0;y = 0;}// A classe Jogador não tem o tipo de decorador mesclado:constjogador = newJogador ();Property 'deveCongelar' does not exist on type 'Jogador'.2339Property 'deveCongelar' does not exist on type 'Jogador'.jogador .; deveCongelar // Se o aspecto do tempo de execução pode ser replicado manualmente via// composição de tipo ou fusão de interface.typeJogadorCongelado = typeofJogador & {deveCongelar : boolean };constjogadorDois = (newJogador () as unknown) asJogadorCongelado ;jogadorDois .deveCongelar ;
Mixins de propriedade estática #17829
Mais uma pegadinha do que uma restrição. O padrão de expressão de classe cria singletons, portanto, não podem ser mapeados no sistema de tipos para suportar diferentes tipos de variáveis.
Você pode contornar isso usando funções para retornar suas classes que diferem com base em um genérico:
tsTry
functionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderivado <T >() {classDerivado extendsbase <T >() {staticoutraProp :T ;}returnDerivado ;}classSpec extendsderivado <string>() {}Spec .prop ; // stringSpec .outraProp ; // string