Digitação nominal no TypeScript ou como proteger sua interface contra identificadores externos


Recentemente, estudando as causas do mau funcionamento do meu projeto em casa, notei mais uma vez um erro que muitas vezes se repete devido à fadiga. A essência do erro é que, tendo vários identificadores em um bloco de código, quando eu chamo uma função, passo o identificador de um objeto de outro tipo. Neste artigo, falarei sobre como resolver esse problema usando o TypeScript.


Pouco de teoria


O TypeScript é baseado na tipagem estrutural, que se encaixa bem na ideologia do pato do JavaScript. Um número suficiente de artigos foi escrito sobre isso. Não os repetirei, apenas descreverei a principal diferença da digitação nominativa, que é mais comum em outros idiomas. Vamos dar uma olhada em um pequeno exemplo.


class Car { id: number; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: number; move (x: number, y: number) { //   } } let car: Car = new Boat(); //  TypeScript   let boat: Boat = new Car(); //        

Por que o TypeScript se comporta dessa maneira? Esta é apenas uma manifestação da tipagem estrutural. Diferentemente do nominativo, que monitora os nomes dos tipos, a tipagem estrutural decide a compatibilidade dos tipos com base em seu conteúdo. A classe Car contém todas as propriedades e métodos da classe Boat, portanto, Car pode ser usado como um barco. O inverso não é verdadeiro porque o Boat não possui a propriedade numberOfWheels.


Identificadores de digitação


Primeiro, definiremos tipos para identificadores


 type CarId: number; type BoatId: number; 

e reescreva as classes usando esses tipos.


 class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: BoatId; move (x: number, y: number) { //   } } 

Você notará que a situação não mudou muito, porque ainda não temos controle sobre de onde obtivemos o identificador e você estará certo. Mas este exemplo já oferece algumas vantagens.


  1. Durante o desenvolvimento do programa, o tipo de identificador pode mudar repentinamente. Assim, por exemplo, um determinado número de carro, exclusivo do projeto, pode ser substituído por um número VIN de string. Sem especificar o tipo de identificador, você terá que substituir o número por uma string em todos os locais onde ocorrer. Com a tarefa de tipo, a alteração precisará ser feita apenas em um local onde o próprio tipo é determinado.


  2. Ao chamar funções, obtemos dicas do nosso editor de código, que identificadores de tipo devem ser. Suponha que tenhamos as seguintes funções declaradas:


     function getCarById(id: CarId): Car { // ... } function getBoatById(id: BoatId): Boat { // ... } 

    Em seguida, obteremos uma dica do editor de que devemos transmitir não apenas um número, mas CarId ou BoatId.



Emule a digitação mais estrita


Não há digitação nominal no TypeScript, mas podemos emular seu comportamento, tornando qualquer tipo exclusivo. Para fazer isso, adicione uma propriedade exclusiva ao tipo. Esse truque é mencionado nos artigos em inglês, sob o termo Branding, e aqui está o que parece:


 type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'}; 

Tendo apontado que nossos tipos devem ser um número e um objeto com uma propriedade com um valor único, tornamos nossos tipos incompatíveis no entendimento da tipagem estrutural. Vamos ver como isso funciona.


 let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId); // OK car = getCarById(boatId); // ERROR boat = getBoatById(boatId); // OK boat = getBoatById(carId); // ERROR carId = 1; // ERROR boatId = 2; // ERROR car = getCarById(3); // ERROR boat = getBoatById(4); // ERROR 

Tudo parece bom, exceto pelas últimas quatro linhas. Para criar identificadores, você precisa de uma função auxiliar:


 function makeCarIdFromVin(id: number): CarId { return vin as any; } 

A desvantagem desse método é que essa função permanecerá em tempo de execução.


Tornando a digitação forte um pouco menos rigorosa


No último exemplo, tive que usar uma função adicional para criar o identificador. Você pode se livrar dele usando a definição da interface Flavor:


 interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>; 

Agora você pode definir tipos para identificadores da seguinte maneira:


 type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”> 

Como a propriedade _type é opcional, você pode usar uma conversão implícita:


 let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK 

E ainda não podemos misturar os identificadores:


 let carId: CarId = boatId; // ERROR 

Qual opção escolher


Ambas as opções têm o direito de existir. A marca tem a vantagem de proteger uma variável da atribuição direta. Isso é útil se a variável armazena a string em algum formato, como um caminho absoluto do arquivo, data ou endereço IP. A função auxiliar que lida com a conversão de tipos nesse caso também pode verificar e processar dados de entrada. Em outros casos, é mais conveniente usar o Sabor.


Fontes


  1. Ponto de partida stackoverflow.com
  2. Interpretação livre do artigo

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


All Articles