Introdução
O TypeScript oferece uma caixa de ferramentas muito rica. Inclui tipos mapeados, tipos condicionais com análise baseada em fluxo de controle, inferência de tipo e muito mais.
Não é uma tarefa fácil para muitos desenvolvedores de JavaScript que são novos no TypeScript mudar de digitação livre para digitação estática. Mesmo para desenvolvedores que trabalham em TypeScript há anos, pode ser confuso, pois o sistema de digitação evolui continuamente.
Um mito comum sobre tipos avançados é que ele deve ser usado principalmente para construir bibliotecas de tipos e não é necessário para o trabalho diário do TypeScript.
A verdade é que os tipos avançados do TypeScript são muito úteis para o trabalho diário do TypeScript. Eles são uma ótima ferramenta para construir um sistema fortemente tipado em seu código, expressando suas intenções claramente e tornando seu código mais seguro.
O propósito de introduzir o conceito de fluxo de tipo é pensar sobre o sistema de digitação em uma maneira que é semelhante a como pensamos sobre o fluxo de dados de programação reativa.
Ao olhar para o sistema de digitação de uma nova perspectiva, isso nos ajudará a”pensar em tipos”e utilizar as ferramentas mais avançadas no Caixa de ferramentas do TypeScript de forma sistemática.
Os tipos podem fluir
Na programação reativa, os dados fluem entre os componentes reativos. No sistema de digitação TypeScript, os tipos também podem fluir.
A primeira vez que encontrei o conceito de “fluxo de tipos” foi em livro TypeScript de PranshuKhandal . Ele explica essa ideia da seguinte maneira:
O fluxo de tipos é exatamente como imagino em meu cérebro o fluxo de informações de tipo.
Inspirado por isso , Eu queria expandir o conceito de fluxo de tipos para o nível do sistema de digitação. Minha definição de tipo de fluxo é:
Tipo de fluxo é quando um ou mais subtipos são mapeados e transformados de um tipo de origem. Esses tipos formam um sistema de digitação fortemente restrito por meio de operações de tipo.
A forma básica de fluxo de tipo pode ser feita por meio de apelidos de tipo.
Os apelidos de tipo permitem que você crie um novo nome de tipo para um tipo existente. No exemplo abaixo, o alias de tipo TargetType é atribuído como uma referência ao SourceType, portanto, o tipo é transferido.
type SourceType={id: string, quantity: number}; tipo TargetType=SourceType;//{id: string, quantidade: número};
Graças ao poder das inferências de tipo, os tipos podem fluir de algumas maneiras diferentes. Isso inclui:
por meio de atribuição de dados Com a função de tipo de retorno, que é inferida pelas instruções de retorno; por exemplo, a função a seguir é inferida para retornar um padrão de tipo de número correspondente aos parâmetros da função: conforme ilustrado no exemplo abaixo, anotar a função de diminuição com o tipo “DecreaseType” converte o valor a, b em um tipo de número
O código a seguir trechos ilustram os casos acima.
quantidade const: número=4; const stockQuantity=quantidade; tipo StockType=typeof stockQuantity;//número//tipo de retorno de função função aumento (a: número) {return a + 1; } resultado const=aumento (2);//número//parâmetros de função correspondentes ao tipo DecreaseType=(início: número, deslocamento: número)=> número; diminuição const: DecreaseType=(a, b)=> {//a: número, b: número return a-b; }
Fluxo de dados de programação reativa vs. fluxo de tipo
O núcleo da programação reativa é o fluxo de dados entre a fonte e os componentes reativos. Alguns de seus conceitos são muito semelhantes ao sistema de digitação TypeScript, como você pode ver no gráfico abaixo.
Você pode ver uma comparação entre os conceitos de operador RxJS e tipagem TypeScript. No sistema de tipagem, o tipo pode ser transformado, filtrado e mapeado para um ou mais subtipos.
Esses subtipos também são “reativos”. Quando o tipo de fonte muda, os subtipos são atualizados automaticamente.
Um sistema de tipo bem projetado adicionará restrições fortemente tipadas aos dados e funções no aplicativo, portanto, quaisquer alterações significativas feitas na definição do tipo de fonte irá mostrar um erro de tempo de compilação imediato.
Embora existam algumas semelhanças entre a tipagem RxJS e TypeScript, ainda existem muitas diferenças entre os dois. Por exemplo, o fluxo de dados em RxJS ocorre em tempo de execução, enquanto o fluxo de tipo em TypeScript ocorre em tempo de compilação.
O objetivo de referenciar RxJS aqui é ilustrar o conceito de fluxo em RxJS, que esperamos nos ajudar a construir uma compreensão compartilhada de “pensar com tipos.”
Operações de tipo em fluxo de tipo
Mapear e filtrar
Os dois operadores mais usados na programação reativa são map e filtrar. Como devemos realizar essas duas operações para tipos no TypeScript?
O tipo mapeado é o equivalente do operador de mapa em RxJS . Ele nos permite criar um tipo baseado em outro tipo usando a assinatura de índice do primeiro e tipos genéricos.
Quando você combina tipos condicionais com inferência de tipo, as transformações de tipo que você pode alcançar com os tipos mapeados estão além imaginação. Discutiremos como usar tipos mapeados posteriormente neste artigo.
O equivalente a matrizes no fluxo de tipos é o tipo de união. Para aplicar filtros em tipos de união, precisamos usar tipos condicionais e o tipo never. Como o filtro é uma necessidade comum, o TypeScript fornece os tipos de utilitário de exclusão e extração prontos para uso.
O código a seguir usa tipos condicionais para remover tipos de T que não são atribuíveis a U. O tipo never é usado aqui para restringir o tipo ou filtrar as opções de um tipo de união.
type Exclude
Também podemos filtrar as propriedades de tipo usando os tipos de utilitário TypeScript prontos para uso.
type Omit
Canalize o fluxo com análise de fluxo de controle
Usando controlar a análise de fluxo com proteção de tipo , podemos canalizar o fluxo da maneira que o operador de canalização faz no RxJS.
Abaixo está um exemplo usando proteção de tipo para realizar verificações de tipo, que restringem o tipo a um mais específico e controlam o fluxo lógico.
function doSomething (x: A | B) {if (x instanceof A) {//x is A} else {//x is B} }
No exemplo acima, o compilador TypeScript analisa todos os fluxos de controle possíveis para a expressão. Ele examina a instância x de A para determinar o tipo de x como A dentro do bloco if e restringe o tipo a B no bloco else.
Se pensarmos nisso como a ramificação lógica, pode ser usado de forma conectiva, semelhante à forma como a água flui por um cano e pode ser redirecionado para um cano conectado diferente para chegar ao seu destino.
Construindo fortes restrições usando o tipo de fluxo
Com a teoria fora do caminho, vamos colocar essa ideia em prática. A seguir, veremos como o conceito de fluxo de tipo pode ser colocado em prática mapeando, filtrando e transformando os tipos para implementar um sistema de tipagem bem restrito.
Definindo os métodos de mapeamento
Estamos construindo um aplicativo Node.js com o padrão mapeador. Para implementar o padrão, devemos primeiro definir alguns métodos mapeadores, que pegam objetos de entidade de dados e os mapeiam para um objeto de transferência de dados (DTO).
Por outro lado, há outro conjunto de métodos para converter os DTOs para os objetos de entidade correspondentes.
export class myMapper {toClient (args: ClientEntity): ClientDto {…}; fromClient (args: ClientDto): ClientEntity {…}; toOrder (args: OrderEntity): OrderDto {…}; fromOrder (args: OrderDto): OrderEntity {…}; }
Usaremos um exemplo simplificado e planejado para demonstrar os dois objetivos a seguir com nosso sistema de tipagem forte:
Criar um tipo para o mapeador com todos os métodos e interfaces do esquema de dados Criar um tipo de união para representam nomes de entidades para segurança de tipo
Definindo a entidade de dados e os tipos de DTO
Primeiro, precisamos definir os tipos de entidades e DTOs.
type DataSchema={client: {dto: {id: string, name: string}, entidade: {clientId: string, clientName: string}}, pedido: {dto: {id: string, amount: number}, entidade: {orderId: string, quantidade: número}} ,}
Agora que temos os tipos de dados brutos definidos, como extraímos cada entidade e tipo de DTO deles?
Usaremos tipos condicionais e nunca para filtrar as definições de tipo de dados necessárias.
digite PropertyType
Podemos simplificar o acima, mesclando-os em um único tipo de pesquisa.
tipo lookup
Suporte para propriedades aninhadas
O tipo de pesquisa acima funciona apenas para uma propriedade de nível único. O que acontece quando o tipo de origem tem mais profundidade?
Para acessar um tipo de propriedade com mais profundidade, criaremos um novo tipo com apelidos de tipo recursivo.
type PropertyType
Quando Path extends keyof T é verdadeiro, significa que o caminho completo é correspondido. Portanto, retornamos o tipo de propriedade atual.
Quando Path extends keyof T é falso, usamos a palavra-chave infer para construir um padrão para combinar com Path. Se corresponder, fazemos uma chamada recursiva para a propriedade do próximo nível. Caso contrário, retornará nunca e isso significa que o caminho não corresponde ao tipo
Se não corresponder, continue recursivamente com a propriedade atual como o primeiro parâmetro.
Definindo o tipo e métodos do mapeador
Agora, é hora de criar os métodos do mapeador. Aqui, usamos tipos literais de string para formar MapTo e MapFrom com a ajuda do tipo de utilitário Capitalize.
//MapTo e MapFrom digite MapTo
Juntando tudo
Quando montamos as partes anteriores, nosso primeiro objetivo é alcançado!
Fazemos uso dos recurso de remapeamento de chave (ou seja, a cláusula as no bloco de código abaixo), que só está disponível desde o lançamento do TypeScript 4.1.
Observe também que Key extends string? Chave: nunca é necessária porque o tipo de chaves de objeto pode variar entre strings, números e símbolos. Estamos interessados apenas nos casos de string aqui.
digite ExtractMapperTo
Podemos ver abaixo que todas as interfaces do método mapeador são criadas automaticamente.
//Nosso primeiro objetivo alcançado! declara const m: mapper; m.toClient ({id:’123′, nome:’John’}); m.fromClient ({clientId:’123′, clientName:’John’}); m.toOrder ({id:’123′, valor: 3}); m.fromOrder ({orderId:’345′, quantidade: 4});
Também temos um bom suporte IDE IntelliSense.
Convertendo um tipo de objeto em um tipo de união
Nosso próximo objetivo é criar um tipo de união para representar os nomes dos tipos de dados do tipo DataSchema de origem.
A chave para a solução é o tipo PropToUnion
//Derive os nomes dos tipos de dados em um tipo de união PropToUnion
Primeiro, {[k in keyof T]: k} extrai a chave de T como chave e valor usando keyof. A saída é:
{client:”client”; pedido:”pedido”; }
Em seguida, usamos a assinatura do índice [keyof T] para extrair os valores como um tipo de união.
O tipo de união gerado pode nos ajudar a reforçar a segurança de tipo. Digamos que colocamos a seguinte função em outro módulo longe do tipo de origem. Na função getProcessName, a instrução switch aciona o protetor de tipo e nunca é retornada no caso padrão para dizer ao compilador que nunca deve ser alcançado.
//Função segundo objetivo alcançado getProcessName (c: DataTypes): string { switch (c) {case’client’: return’register’+ c; case’pedido’: retorna’processo’+ c; padrão: return assertUnreachable (c); }} function assertUnreachable (x: never): never {throw new Error (“algo está muito errado”); }
É assim que o tipo de união e nunca ajuda a impor a segurança de tipo.
Agora, vamos supor que houve uma mudança no esquema de dados-adicionamos um novo tipo de dados chamado conta. Em uma equipe grande, o desenvolvedor que adiciona o novo tipo pode não estar ciente do impacto da mudança. Sem restrições de digitação, isso pode resultar em um erro oculto de tempo de execução que é difícil de encontrar.
Se usarmos o fluxo de tipo para construir as restrições de digitação, os subtipos de downstream serão atualizados automaticamente como abaixo.
tipo DataTypes=”cliente”|”ordem”|”Account”
O compilador TypeScript também mostrará um erro na função getProcessName para nos avisar que ocorreu uma alteração importante.
Nosso segundo objetivo foi alcançado! Temos um tipo de união que agora representa nomes de entidades e contribui para a segurança do tipo.
Para recapitular, este diagrama mostra as principais etapas que tomamos para atingir o primeiro objetivo do fluxo de tipo.
No geral, criamos vários novos tipos com base no tipo de fonte original. Quaisquer mudanças no tipo de fonte irão acionar atualizações para todos os tipos de downstream automaticamente e receberemos um prompt de erro instantâneo se a mudança quebrar as funções que dependem dela.
O código de exemplo completo pode ser encontrado em o Gist abaixo .
Resumo
Este artigo discute o conceito de fluxo de tipo TypeScript com referência à programação reativa em RxJS. Aplicamos o conceito de fluxo de tipos a um exemplo prático, construindo um sistema de tipos bem restrito para maximizar os benefícios da segurança de tipos.
Espero que esta discussão ajude a mudar a ideia de que os tipos avançados do TypeScript são apenas para desenvolvimento bibliotecas de tipos ou para programação complexa em nível de estrutura. Também espero que ele possa ajudá-lo a começar a aplicar o sistema de digitação de forma mais criativa em seu trabalho diário de TypeScript.
Boa digitação!