Transfira 30.000 linhas de código do Flow para o TypeScript

Recentemente, transferimos 30.000 linhas de código JavaScript do nosso sistema MemSQL Studio do Flow para o TypeScript. Neste artigo, explicarei por que portamos a base de código, como aconteceu e o que aconteceu.

Isenção de responsabilidade: meu objetivo não é criticar a Flow. Admiro o projeto e acho que há espaço suficiente na comunidade JavaScript para as duas opções de verificação de tipo. No final, todos escolherão o que melhor lhe convier. Espero sinceramente que o artigo ajude nessa escolha.

Primeiro, vou atualizá-lo. No MemSQL, somos grandes fãs da digitação estática e forte do JavaScript para evitar problemas comuns com a digitação dinâmica e fraca.

Discurso sobre problemas comuns:

  1. Digite erros no tempo de execução devido ao fato de que diferentes partes do código não são correspondidas por tipos implícitos.
  2. É gasto muito tempo escrevendo testes para coisas triviais, como a verificação de parâmetros de tipo (a verificação no tempo de execução também aumenta o tamanho do pacote).
  3. Há uma falta de integração editor / IDE, porque sem a digitação estática, é muito mais difícil implementar a função Ir para a definição, refatoração mecânica e outras funções.
  4. Não há como escrever código em torno de modelos de dados, ou seja, primeiro projetar tipos de dados e, em seguida, o código basicamente "se escreve".

Esses são apenas alguns dos benefícios da digitação estática, listados em um artigo recente sobre o Flow .

No início de 2016, implementamos o tcomb para implementar algum tipo de segurança no tempo de execução de um de nossos projetos JavaScript internos (aviso de isenção de responsabilidade: não lidei com esse projeto). Embora a verificação em tempo de execução às vezes seja útil, ela nem fornece todos os benefícios da digitação estática (a combinação de digitação estática e verificação de tipo em tempo de execução pode ser adequada para certos casos, o io-ts permite fazer isso com tcomb e TypeScript, embora eu nunca tenha tentado ) Entendendo isso, decidimos implementar o Flow para outro projeto que iniciamos em 2016. Naquela época, o Flow parecia uma ótima opção:

  • Suporte do Facebook, que fez um trabalho incrível no desenvolvimento do React e no crescimento da comunidade (eles também desenvolveram o React with Flow).
  • Aproximadamente o mesmo ecossistema de desenvolvimento JavaScript. Foi assustador abandonar o Babel for tsc (compilador TypeScript) porque perdemos a flexibilidade de mudar para outra verificação de tipo (obviamente, a situação mudou desde então).
  • Não há necessidade de digitar toda a base de código (queríamos ter uma idéia do JavaScript digitado estaticamente antes de entrar no programa), mas apenas parte dos arquivos. Observe que agora o Flow e o TypeScript permitem isso.
  • O TypeScript (na época) carecia de algumas das funções básicas que estão disponíveis agora, são tipos de pesquisa , parâmetros padrão para tipos genéricos etc.

Quando começamos a trabalhar no MemSQL Studio, no final de 2017, cobriríamos tipos de todo o aplicativo (ele é inteiramente escrito em JavaScript: o frontend e o backend são executados no navegador). Tomamos o Flow como uma ferramenta que usamos com sucesso no passado.

Mas minha atenção foi atraída para o Babel 7 com suporte ao TypeScript . Esta versão significava que a mudança para o TypeScript não exigia mais uma transição para todo o ecossistema do TypeScript, e você poderia continuar usando o Babel para JavaScript. Mais importante, poderíamos usar o TypeScript apenas para verificação de tipo , e não como uma "linguagem" completa.

Pessoalmente, acredito que separar a verificação de tipo do gerador de código é uma maneira mais elegante de digitar estática (e forte) em JavaScript, porque:

  1. Compartilhamos os problemas de código e digitação. Isso reduz as paradas de verificação e acelera o desenvolvimento: se por algum motivo a verificação de tipo for lenta, o código ainda será gerado corretamente (se você usar tsc com Babel, poderá configurá-lo para fazer o mesmo).
  2. Babel possui ótimos plugins e recursos que o gerador TypeScript não possui. Por exemplo, o Babel permite que você especifique os navegadores suportados e emite automaticamente o código para eles. Essa é uma função muito complexa e não faz sentido apoiá-la em dois projetos diferentes ao mesmo tempo.
  3. Eu gosto do JavaScript como uma linguagem de programação (exceto pela falta de digitação estática) e não tenho idéia do quanto o TypeScript existirá, enquanto acredito em muitos anos de ECMAScript. Portanto, prefiro escrever e "pensar" em JavaScript (observe que digo "use Flow" ou "use TypeScript" em vez de "write in Flow" ou "TypeScript", porque eu sempre os represento com ferramentas, não com linguagens de programação).

Obviamente, essa abordagem tem algumas desvantagens:

  1. O compilador TypeScript pode teoricamente executar otimizações baseadas em tipo, mas aqui perdemos essa oportunidade.
  2. A configuração do projeto é um pouco mais complicada com um aumento no número de ferramentas e dependências. Penso que este é um argumento relativamente fraco: um monte de Babel e Flow nunca nos decepcionaram.

TypeScript como uma alternativa ao Flow


Percebi um crescente interesse no TypeScript na comunidade JavaScript: online e entre os desenvolvedores. Portanto, assim que descobri que o Babel 7 suporta TypeScript, comecei imediatamente a estudar possíveis opções de transição. Além disso, encontramos algumas das desvantagens do Flow:

  1. Menor qualidade da integração editor / IDE (em comparação com o TypeScript). O Nuclide, o IDE do Facebook com a melhor integração, já está desatualizado.
  2. Uma comunidade menor, que significa menos definições de tipo para bibliotecas diferentes, e elas são de qualidade inferior (atualmente o repositório DefinitelyTyped possui 19 682 estrelas do GitHub e o repositório com tipo de fluxo possui apenas 3070).
  3. Falta de um plano de desenvolvimento público e pouca interação entre a equipe do Flow no Facebook e a comunidade. Você pode ler este comentário de um funcionário do Facebook para entender a situação.
  4. Alto consumo de memória e vazamentos frequentes - para alguns de nossos desenvolvedores, o Flow às vezes ocupava quase 10 GB de RAM.

Obviamente, você deve estudar como o TypeScript nos convém. Essa é uma pergunta muito complexa: o estudo do tópico incluiu uma leitura completa da documentação, o que ajudou a entender que para todas as funções do Flow existe um TypeScript equivalente. Depois, explorei o plano de desenvolvimento público do TypeScript e gostei muito dos recursos planejados para o futuro (por exemplo, derivação parcial dos argumentos de tipo que usamos no Flow).

Transfira mais de 30 mil linhas de código do Flow para o TypeScript


Para iniciantes, você deve atualizar o Babel de 6 para 7. Essa tarefa simples levou 16 horas-homem, pois decidimos atualizar o Webpack 3 para 4. Ao mesmo tempo, algumas dependências obsoletas em nosso código complicaram a tarefa. A grande maioria dos projetos JavaScript não terá esses problemas.

Depois disso, substituímos a predefinição Babel Flow pela nova predefinição TypeScript e, pela primeira vez, executamos o compilador TypeScript em todas as nossas fontes escritas com Flow. O resultado são erros de sintaxe 8245 (o tsc CLI não mostra erros reais para o projeto até que todos os erros de sintaxe tenham sido corrigidos).

A princípio, esse número nos assustou (muito), mas rapidamente percebemos que a maioria dos erros se devia ao TypeScript não suportar arquivos .js. Tendo estudado o tópico, aprendi que os arquivos TypeScript devem terminar com .ts ou .tsx (se eles tiverem JSX). Isso me parece um claro inconveniente. Para não pensar na presença / ausência do JSX, simplesmente renomeei todos os arquivos para .tsx.

Cerca de 4.000 erros de sintaxe permanecem. A maioria deles está relacionada ao tipo de importação , que com o TypeScript pode ser substituído simplesmente pela importação, bem como à diferença na designação de objetos ( {||} vez de {} ). Aplicando rapidamente algumas expressões regulares, deixamos 414 erros de sintaxe. Tudo o resto tinha que ser corrigido manualmente:

  • O tipo existencial , que usamos para derivar parcialmente argumentos de um tipo genérico, deve ser substituído por argumentos explícitos ou desconhecidos para informar ao TypeScript que alguns argumentos não são importantes.
  • Digite $ Keys e outros tipos avançados de fluxo têm uma sintaxe diferente no TypeScript (por exemplo, $Shape“” corresponde a Partial“” no TypeScript).

Depois de corrigir todos os erros de sintaxe, o tsc finalmente disse quantos erros de tipo real em nossa base de código são apenas 1300. Agora tivemos que sentar e decidir se continuaria ou não. Afinal, se a migração demorar semanas, é melhor permanecer no Flow. No entanto, decidimos que a portabilidade de código exigiria menos de uma semana de trabalho por um engenheiro, o que é bastante aceitável.

Observe que, durante a migração, tive que interromper todo o trabalho nessa base de código. No entanto, em paralelo, você pode iniciar novos projetos - mas você deve ter em mente centenas de erros de tipo no código existente, o que não é fácil.

Que tipo de erros?


O TypeScript e o Flow processam o código JavaScript de várias maneiras. Portanto, o Flow é mais rigoroso em relação a algumas coisas e o TypeScript - em relação a outras. Uma comparação profunda dos dois sistemas será muito longa, então veja alguns exemplos.

Nota: todos os links para a sandbox TypeScript assumem parâmetros "estritos". Infelizmente, quando você compartilha um link, essas opções não são armazenadas no URL. Portanto, eles devem ser definidos manualmente após abrir qualquer link para a sandbox deste artigo.

invariant.js


A função invariant acabou sendo muito comum em nosso código fonte. Apenas para citar a documentação:

 var invariant = require('invariant'); invariant(someTruthyVal, 'This will not throw'); // No errors invariant(someFalseyVal, 'This will throw an error with this message'); // Error raised: Invariant Violation: This will throw an error with this message 

A ideia é clara: uma função simples que gera um erro em alguma condição. Vamos ver como implementá-lo e usá-lo no Flow:

 type Maybe<T> = T | void; function invariant(condition: boolean, message: string) { if (!condition) { throw new Error(message); } } function f(x: Maybe<number>, c: number) { if (c > 0) { invariant(x !== undefined, "When c is positive, x should never be undefined"); (x + 1); // works because x has been refined to "number" } } 

Agora carregue o mesmo trecho no TypeScript . Como você pode ver no link, o TypeScript comete um erro, porque não é possível entender que x garantido para não permanecer undefined após a última linha. Na verdade, esse é um problema bem conhecido - o TypeScript (por enquanto) não sabe como fazer essa inferência por meio de uma função. No entanto, este é um modelo muito comum em nossa base de código, então tive que substituir manualmente cada instância invariável (mais de 150 partes) por outro código que imediatamente emitisse um erro:

 type Maybe<T> = T | void; function f(x: Maybe<number>, c: number) { if (c > 0) { if (x === undefined) { throw new Error("When c is positive, x should never be undefined"); } (x + 1); // works because x has been refined to "number" } } 

Não é realmente comparado a invariant , mas não é uma questão tão importante.

$ ExpectError vs @ ts-ignore


O fluxo tem uma função muito interessante, semelhante a @ts-ignore , exceto que gera um erro se a próxima linha não for um erro. Isso é muito útil para escrever "testes de tipo" que garantem que a verificação de tipo (seja TypeScript ou Flow) encontre certos erros de tipo.

Infelizmente, o TypeScript não possui essa função, portanto nossos testes perderam algum valor. Estou ansioso para implementar esta função no TypeScript .

Erros de tipo genérico e inferência de tipo


Geralmente, o TypeScript permite um código mais explícito que o Flow, como neste exemplo:

 type Leaf = { host: string; port: number; type: "LEAF"; }; type Aggregator = { host: string; port: number; type: "AGGREGATOR"; } type MemsqlNode = Leaf | Aggregator; function f(leaves: Array<Leaf>, aggregators: Array<Aggregator>): Array<MemsqlNode> { // The next line errors because you cannot concat aggregators to leaves. return leaves.concat(aggregators); } 

O fluxo deduz o tipo leaves.concat (agregadores) como Matriz <Folha | Agregador> , que pode ser Array<MemsqlNode> na Array<MemsqlNode> . Acho que este é um bom exemplo em que o Flow pode ser um pouco mais inteligente e o TypeScript precisa de uma pequena ajuda: nesse caso, podemos aplicar uma asserção de tipo, mas isso é perigoso e deve ser feito com muito cuidado.

Embora eu não tenha nenhuma evidência formal, acredito que o Flow seja muito superior ao TypeScript na inferência de tipo. Eu realmente espero que o TypeScript atinja o nível Flow, pois a linguagem está se desenvolvendo muito ativamente e muitas melhorias recentes foram feitas nessa área. Em muitos lugares do nosso código, o TypeScript teve que ajudar um pouco nas anotações ou nas afirmações de tipo, embora tenhamos evitado o máximo possível. Vamos considerar mais um exemplo (tivemos mais de 200 desses erros):

 type Player = { name: string; age: number; position: "STRIKER" | "GOALKEEPER", }; type F = () => Promise<Array<Player>>; const f1: F = () => { return Promise.all([ { name: "David Gomes", age: 23, position: "GOALKEEPER", }, { name: "Cristiano Ronaldo", age: 33, position: "STRIKER", } ]); }; 

O TypeScript não permitirá que você escreva isso porque não permitirá que você declare { name: "David Gomes", age: 23, type: "GOALKEEPER" } como um objeto do tipo Player (consulte o sandbox para obter o erro exato). Este é outro caso em que considero que o TypeScript não é inteligente o suficiente (pelo menos em comparação com o Flow, que entende esse código).

Existem várias opções para corrigir isso:

  • Declare "STRIKER" como "STRIKER" para que TypeScript entenda que a string é uma enumeração válida do tipo "STRIKER" | "GOALKEEPER" "STRIKER" | "GOALKEEPER" .
  • Declare todos os objetos como Player .
  • Ou o que considero ser a melhor solução: ajude o TypeScript sem usar nenhuma instrução de tipo escrevendo Promise.all<Player>(...) .

Aqui está outro exemplo (TypeScript) em que o Flow é novamente melhor na inferência de tipo :

 type Connection = { id: number }; declare function getConnection(): Connection; function resolveConnection() { return new Promise(resolve => { return resolve(getConnection()); }) } resolveConnection().then(conn => { // TypeScript errors in the next line because it does not understand // that conn is of type Connection. We have to manually annotate // resolveConnection as Promise<Connection>. (conn.id); }); 

Um exemplo muito pequeno, mas interessante: o fluxo considera a Array<T>.pop() tipo T e o TypeScript considera como T | void T | void Um ponto a favor do TypeScript, porque obriga a verificar duas vezes a existência de um elemento (se a matriz estiver vazia, então Array.pop retornará undefined ). Existem vários outros pequenos exemplos como este, em que o TypeScript é superior ao Flow.

Definições TypeScript para dependências de terceiros


Obviamente, ao escrever qualquer aplicativo JavaScript, você terá pelo menos algumas dependências. Eles devem ser digitados, caso contrário, você perderá a maioria das possibilidades de análise de tipo estático (conforme descrito no início do artigo).

As bibliotecas do npm podem vir com definições de tipo Flow ou TypeScript, com ou sem ambas. Freqüentemente (pequenas) bibliotecas não são fornecidas com uma ou outra, portanto, você deve escrever suas próprias definições de tipo ou emprestá-las da comunidade. O Flow e o TypeScript suportam repositórios de definição padrão para pacotes JavaScript de terceiros: são do tipo flow e DefinitelyTyped .

Devo dizer que DefinitelyTyped gostamos muito mais. Com o tipo de fluxo, tive que usar a ferramenta CLI para introduzir definições de tipo para várias dependências no projeto. DefinitelyTyped combina essa função com a ferramenta CLI npm enviando @types/package-name packages para o repositório de pacotes npm. Isso é muito legal e simplificou bastante a entrada de definições de tipo para nossas dependências (brincadeira, reação, lodash, reação-redux, essas são apenas algumas).

Além disso, eu me diverti bastante preenchendo o banco de dados DefinitelyTyped (não pense que as definições de tipo sejam equivalentes ao transportar código do Flow para o TypeScript). enviei algumas solicitações pull e não houve problemas em nenhum lugar. Apenas clone o repositório, edite as definições de tipo, adicione testes - e envie uma solicitação pull. O bot do GitHub DefinitelyTyped marca os autores das definições que você editou. Se nenhum deles fornecer feedback dentro de sete dias, a solicitação de recebimento será enviada para consideração do mantenedor. Após mesclar com a ramificação principal, uma nova versão do pacote de dependência é enviada para o npm. Por exemplo, quando atualizei o pacote @ types / redux-form pela primeira vez, a versão 7.4.14 foi enviada automaticamente para o npm. portanto, basta atualizar o arquivo package.json para obter novas definições de tipo. Se você não puder esperar a adoção da solicitação de recebimento, sempre poderá alterar as definições dos tipos usados ​​no seu projeto, conforme descrito em um dos artigos anteriores .

Em geral, a qualidade das definições de tipo no DefinitelyTyped é muito melhor devido à comunidade TypeScript maior e mais próspera. De fato, após a transferência do projeto para o TypeScript , nossa cobertura de tipo aumentou de 88% para 96% , principalmente devido a melhores definições de tipos de dependência de terceiros, com menos tipos.

Fiapos e testes


  1. Mudamos de eslint para tslint (com eslint para TypeScript, parecia mais difícil começar).
  2. Os testes TypeScript usam ts-jest . Alguns dos testes são digitados, enquanto outros não (se digitados por muito tempo, os salvamos como arquivos .js).

O que aconteceu depois de corrigir todos os erros de digitação?


Após 40 horas de trabalho, alcançamos o último erro de digitação, adiando-o por um tempo usando @ts-ignore .

Depois de revisar os comentários de revisão de código e corrigir alguns bugs (infelizmente, tive que alterar um pouco o código de tempo de execução para corrigir a lógica que o TypeScript não conseguia entender), a solicitação pull desapareceu e, desde então, usamos o TypeScript. (E sim, corrigimos o último @ts-ignore na próxima solicitação de recebimento).

Além da integração com o editor, o trabalho com o TypeScript é muito semelhante ao trabalho com o Flow. O desempenho do servidor de fluxo é um pouco maior, mas esse não é um grande problema, porque eles geram erros para o arquivo atual de maneira igualmente rápida. A única diferença de desempenho é que o TypeScript relata um novo erro após salvar o arquivo um pouco mais tarde (de 0,5 a 1 s). O tempo de inicialização do servidor é aproximadamente o mesmo (cerca de 2 minutos), mas não é tão importante. Até agora, não tivemos problemas com o consumo de memória. Parece que o tsc usa constantemente cerca de 600 MB.

Pode parecer que a função de inferência de tipo dê ao Flow uma grande vantagem, mas há duas razões pelas quais isso realmente não importa:

  1. Convertemos a base de código do Flow em TypeScript. Obviamente, encontramos apenas o código que o Flow pode expressar, mas o TypeScript não. Se a migração tivesse acontecido na direção oposta, tenho certeza de que haveria coisas que o TypeScript melhor exibe / expressa.
  2. A inferência de tipo é importante para ajudar a escrever um código mais conciso. Mas, ainda assim, outras coisas são mais importantes, como uma comunidade forte e a disponibilidade de definições de tipo, porque a inferência de tipo fraca pode ser corrigida gastando um pouco mais de tempo na digitação.

Estatísticas de código


 $ npm run type-coverage # https://github.com/plantain-00/type-coverage 43330 / 45047 96.19% $ cloc # ignoring tests and dependencies -------------------------------------------------------------------------------- Language files blank comment code -------------------------------------------------------------------------------- TypeScript 330 5179 1405 31463 

O que vem a seguir?


Não terminamos de melhorar a análise de tipo estático. O MemSQL tem outros projetos que eventualmente mudarão do Flow para o TypeScript (e alguns projetos JavaScript que começarão a usar o TypeScript), e queremos tornar nossa configuração do TypeScript mais rigorosa. Atualmente, temos a opção strictNullChecks ativada , mas noImplicitAny ainda está desativado. Também removeremos algumas instruções de tipo perigoso do código.

É um prazer compartilhar com vocês tudo o que aprendi durante minhas aventuras com a digitação de JavaScript. Se você estiver interessado em um tópico específico, entre em contato .

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


All Articles