Olá pessoal! Meu nome é Dmitry Novikov, sou desenvolvedor de javascript no Alfa Bank e hoje vou falar sobre nossa experiência em derivar tipos de ação usando o Typecript, quais problemas encontramos e como resolvemos.
Esta é uma transcrição do meu relatório no Alfa JavaScript MeetUp. Você pode ver o código dos slides da apresentação
aqui e a gravação da transmissão mitap
aqui .
Nossos aplicativos front-end são executados em um monte de React + Redux. O fluxo de dados Redux simplesmente se parece com isso:

Existem criadores de ação - funções que retornam uma ação. As ações caem no redutor, o redutor cria um novo lado com base no antigo. Os componentes são assinados pela parte que, por sua vez, pode despachar novas ações - e tudo se repete.
É assim que o criador da ação fica no código:

Esta é apenas uma função que retorna uma ação - um objeto que deve ter um campo de sequência de caracteres e alguns dados (opcional).
É assim que um redutor típico se parece:

Este é um caso de comutação regular que analisa o campo de tipo de uma ação e gera um novo lado. No exemplo acima, ele simplesmente adiciona os valores da propriedade da ação lá.
E se acidentalmente cometemos um erro ao escrever um redutor? Por exemplo, assim, trocaremos as propriedades de diferentes ações:

O Javascript não sabe nada sobre nossas ações e considera esse código absolutamente válido. No entanto, não funcionará como pretendido e gostaríamos de ver esse erro. O que nos ajudará se não estiver datilografado? Vamos tentar tipificar nossas ações.

Para começar, escreveremos tipos de "testa" para nossas ações - Action1Type e Action2Type. E então, combine-os em um tipo de união para usar no redutor. A abordagem é simples e direta, mas e se os dados nas ações mudarem durante o desenvolvimento do aplicativo? Não altere os tipos manualmente sempre. Nós os reescrevemos da seguinte maneira:

O operador typeof retornará o tipo de criador da ação para nós e ReturnType nos fornecerá o tipo do valor de retorno da função - ou seja, tipo de ação. Como resultado, o resultado será o mesmo do slide acima, mas não mais manualmente - ao alterar as ações, os ActionTypes do tipo união serão atualizados automaticamente. Uau! Nós escrevemos no redutor e ...

E imediatamente obtemos erros do script. Além disso, os erros não são totalmente claros - a propriedade bar está ausente na ação foo e foo está ausente na barra ... Parece ser do jeito que deveria ser? Algo parece estar bagunçado. Em geral, a abordagem da testa não funciona conforme o esperado.
Mas este não é o único problema. Imagine que, com o tempo, nosso aplicativo crescerá e teremos muitas ações. Muito.

Como seria nosso tipo comum nesse caso? Provavelmente algo como isto:

E se levarmos em conta que as ações serão adicionadas e excluídas, teremos que suportar tudo isso manualmente - adicione e exclua tipos. Isso também não nos convém. O que fazer Vamos começar com o primeiro problema.

Portanto, temos alguns criadores de ação, e o tipo comum para eles é a união de tipos de ação derivados automaticamente. Cada ação possui uma propriedade de tipo e é definida como uma sequência. Essa é a raiz do problema. Para distinguir uma ação de outra, precisamos que cada tipo seja único e aceite apenas um valor único.

Este tipo é chamado literal. O tipo literal é de três tipos - numérico, string e booleano.

Por exemplo, temos o tipo onlyNumberOne e especificamos que uma variável desse tipo pode ser igual apenas ao número 1. Atribua 2 - e obtenha um erro de digitação. String funciona de maneira semelhante - apenas um valor específico de string pode ser atribuído a uma variável. Bem, booleano é verdadeiro ou falso, sem ambiguidade.
Genérico
Como salvar esse tipo sem permitir que ele se transforme em uma string? Nós usaremos genéricos. Genérico é uma abstração sobre tipos. Suponha que tenhamos uma função inútil que recebe uma entrada como argumento e a retorna sem alterações. Como posso digitar? Escreva alguma, porque pode ser absolutamente qualquer tipo? Mas se algum tipo de lógica estiver presente na função, poderá ocorrer a conversão do tipo e, por exemplo, um número poderá se transformar em uma string, e qualquer combinação qualquer ignorará isso. Não é adequado.

Um genérico nos ajudará a sair dessa situação. A entrada acima significa que estamos passando um argumento de algum tipo T, e a função retornará exatamente o mesmo tipo T. Não sabemos qual será - um número, uma string, booleano ou qualquer outra coisa - mas podemos garantir que será exatamente o mesmo tipo. Esta opção nos convém.
Vamos desenvolver um pouco o conceito de genéricos. Precisamos processar nem todos os tipos em geral, mas uma string de concreto literal. Existe uma palavra-chave extends para isso:

A notação “T estende a string” significa que T é um determinado tipo, que é um subconjunto do tipo de string. Vale a pena notar que isso funciona apenas com tipos primitivos - se, em vez de usar string, usarmos um tipo de objeto com um conjunto específico de propriedades, pelo contrário, significa que T é um conjunto OVER desse tipo.
Abaixo estão exemplos de uso de uma função digitada com extends e genéricos:

- Argumento do tipo string - a função retornará string
- Um argumento do tipo string literal - a função retornará string literal
- Se o argumento não se parecer com uma sequência, por exemplo, um número ou uma matriz, o script apresentará um erro.
Bem, e no geral funciona.

Substituímos nossa função no tipo de ação - ela retorna exatamente o mesmo tipo de string, mas não é mais uma string, mas uma literal, como deveria ser. Coletamos o tipo de união, tipificamos um redutor - está tudo bem. E se cometermos um erro e escrevermos as propriedades erradas, o script de tempo fornecerá não dois, mas um erro lógico e compreensível:

Vamos um pouco mais além e abstratos do tipo string. Escreveremos a mesma tipificação, usando apenas dois genéricos - T e U. Agora, temos um certo tipo de T que dependerá de outro tipo de U, em vez do qual podemos usar qualquer coisa - pelo menos string, pelo menos número, pelo menos booleano. Isso é implementado usando a função wrapper:

E finalmente: o problema descrito permaneceu por muito tempo como problema no github e, finalmente, no Typescript versão 3.4, os desenvolvedores nos apresentaram uma solução - afirmação constante. Tem duas formas de gravação:

Portanto, se você tiver um texto datilografado novo, poderá simplesmente usar como const nas ações, e o tipo literal não se transformará em uma string. Nas versões mais antigas, você pode usar o método descrito acima. Acontece que agora temos duas soluções para o primeiro problema. Mas o segundo permanece.

Ainda temos muitas ações diferentes e, apesar de sabermos agora como lidar com seus tipos corretamente, ainda não sabemos como montá-los automaticamente. Podemos escrever a união manualmente, mas se as ações forem excluídas e adicionadas, ainda precisamos excluir e adicioná-las manualmente no tipo Isto está errado.

Por onde começar? Suponha que tenhamos criadores de ação importados juntos de um único arquivo. Gostaríamos de contorná-los um por um, deduzir os tipos de suas ações e coletá-los em um tipo de união. E o mais importante, gostaríamos de fazer isso automaticamente, sem editar tipos manualmente.

Vamos começar analisando os criadores de ação. Para fazer isso, existe um tipo mapeado especial que descreve as coleções de valores-chave. Aqui está um exemplo:

Isso cria um tipo para um objeto cujas chaves são a opção1 e a opção2 (do conjunto de Chaves) e os valores são verdadeiros ou falsos. Em uma versão mais geral, isso pode ser representado como um tipo de mapOfBool - um objeto com algum tipo de chave de linha e valores booleanos.
Bom Mas como podemos verificar se é um objeto que nos é dado na entrada e não outro tipo? O tipo condicional, um ternário simples no mundo dos tipos, nos ajudará com isso.

Neste exemplo, verificamos: o tipo T tem algo em comum com a string? Se sim, retorne string e, se não, nunca retorne. Este é um tipo tão especial que sempre nos retornará um erro. O literal de string satisfaz a condição ternária. Aqui estão alguns exemplos de código:

Se especificarmos algo nos genéricos que não seja como string, o texto datilografado nos dará um erro.
Descobrimos a solução alternativa e a verificação, resta apenas obter os tipos e fundi-los em união. Isso nos ajudará a inferir a inferência de tipo no texto datilografado. Infer geralmente vive em um tipo condicional e faz algo assim: ele percorre todos os pares de valores-chave, tenta inferir o tipo de valor e o compara com os outros. Se os tipos de valores forem diferentes, ele os combinará em uma união. Apenas o que precisamos!

Bem, agora resta juntar tudo.
Acontece que este design:

A lógica é aproximadamente a seguinte: Se T se parece com um objeto que possui algumas chaves de seqüência de caracteres (nomes de criadores de ações) e eles possuem valores de algum tipo (uma função que nos retornará a ação), tente ignorar esses pares e deduzir o tipo desses valores e reduza seu tipo comum. E se algo der errado - lance um erro especial (digite nunca).
É difícil apenas à primeira vista. De fato, tudo é bem simples. Vale a pena prestar atenção a um recurso interessante - devido ao fato de que cada ação tem um campo de tipo exclusivo, os tipos dessas ações não se mantêm unidas e obtemos um tipo de união completo na saída. Aqui está o que parece no código:

Importamos os criadores da ação como ações, pegamos o ReturnType (o tipo de valor de retorno é ações) e coletamos usando nosso tipo especial. Acontece exatamente o que era necessário.

Qual é o resultado? Temos união de tipos literais para todas as ações. Quando uma nova ação é adicionada, o tipo é atualizado automaticamente. Como resultado, obtemos uma digitação rigorosa de ações, agora não podemos cometer erros. Bem, ao longo do caminho, aprendemos sobre genéricos, tipo condicional, tipo mapeado, nunca e inferir - você pode obter ainda mais informações sobre essas ferramentas
aqui .