Portando APIs para TypeScript como um solucionador de problemas

O frontend React do programa Execute foi convertido de JavaScript para TypeScript. Mas o back-end, escrito em Ruby, não tocou. No entanto, os problemas associados a esse back-end fizeram os desenvolvedores do projeto pensar em mudar do Ruby para o TypeScript. A tradução do material que estamos publicando hoje é dedicada à história de portar o back-end do Execute Program do Ruby para o TypeScript e a quais problemas isso ajudou a resolver.



Usando o back-end do Ruby, às vezes esquecemos que algumas propriedades da API armazenam uma matriz de seqüências de caracteres, não uma sequência simples. Às vezes, alteramos um fragmento da API que era acessado em locais diferentes, mas esquecemos de atualizar o código em um desses locais. Esses são os problemas comuns de uma linguagem dinâmica, característicos de qualquer sistema cujo código não é 100% coberto por testes. (Embora menos comum, isso ocorre quando o código é totalmente coberto por testes.)

Ao mesmo tempo, esses problemas desapareceram do front-end desde que o trocamos para o TypeScript. Tenho mais experiência em programação de servidores do que em cliente, mas, apesar disso, cometi mais erros ao trabalhar com o back-end, e não com o front-end. Tudo isso indicava que o back-end também deveria ser convertido para TypeScript.

Portamos o back-end do Ruby para o TypeScript em março de 2019 em cerca de duas semanas. E tudo funcionou como deveria! Implantamos um novo código em produção em 14 de abril de 2019. Era uma versão beta disponível para um número limitado de usuários. Depois disso, nada quebrou. Os usuários nem perceberam nada. Aqui está um gráfico que ilustra o estado da nossa base de código antes e imediatamente após a transição. O eixo x representa o tempo (em dias), o eixo y representa o número de linhas de código.


Traduzindo o frontend de JavaScript para TypeScript e traduzindo o back-end de Ruby para TypeScript

Durante o processo de portabilidade, escrevi uma grande quantidade de código auxiliar. Portanto, temos nossa própria ferramenta para executar testes com um volume de 200 linhas. Temos uma biblioteca de 120 linhas para trabalhar com o banco de dados, bem como uma biblioteca de roteamento maior para a API, vinculando o código de front-end e back-end.

Em nossa própria infraestrutura, o mais interessante é o roteador. É um wrapper para o Express, garantindo a aplicação correta dos tipos usados ​​no código do cliente e do servidor. Isso significa que, quando uma parte da API é alterada, a outra nem é compilada sem fazer alterações para eliminar as diferenças.

Aqui está um manipulador de back-end que retorna uma lista de postagens do blog. Este é um dos fragmentos de código semelhantes mais simples do sistema:

router.handleGet(api.blog, async () => {
  return {
    posts: blog.posts,
  }
})

Se mudarmos o nome da chave postspara blogPosts, obteremos um erro de compilação, cujo texto é mostrado abaixo (aqui, por questões de brevidade, as informações sobre os tipos de objetos são omitidas.)

Property 'posts' is missing in type '...' but required in type '...'.

Cada terminal é definido por um objeto de visualização api.someNameHere. Este objeto é compartilhado pelo cliente e servidor. Observe que os tipos não são mencionados diretamente na declaração do manipulador. Todos eles são inferidos a partir do argumento api.blog.

Essa abordagem funciona para terminais simples, como o terminal descrito acima blog. Mas é adequado para terminais mais complexos. Por exemplo, uma API de terminal para trabalhar com lições possui uma chave profundamente aninhada de um tipo lógico .lesson.steps[index].isInteractive. Graças a tudo isso, agora é impossível cometer os seguintes erros:

  • Se tentarmos acessar isinteractiveo cliente ou tentar retornar essa chave do servidor, o código não será compilado. O nome da chave deve ser parecido isInteractivecom uma capital I.
  • isInteractive — .
  • isInteractive number, , , .
  • API, , isInteractive — , , , , , , , .

Observe que tudo isso inclui geração de código. Isso é feito usando io-ts e algumas centenas de linhas de código de nosso próprio roteador.

Declarar tipos de API requer trabalho adicional, mas o trabalho é simples. Ao alterar a estrutura da API, precisamos saber como a estrutura do código muda. Fazemos alterações nas declarações da API e, em seguida, o compilador nos aponta para todos os locais onde o código precisa ser corrigido.

É difícil apreciar a importância desses mecanismos até você usá-los por um tempo. Podemos mover objetos grandes de um lugar na API para outro, renomear as chaves, podemos dividir objetos grandes em partes, mesclar objetos pequenos em um objeto, dividir ou mesclar pontos de extremidade inteiros. E podemos fazer tudo isso sem nos preocupar com o fato de que esquecemos de fazer as alterações apropriadas no código do cliente ou servidor.

Aqui está um exemplo real. Recentemente, passei cerca de 20 horas em quatro dias reprojetando o API Execute Program . Toda a estrutura da API foi alterada. Ao comparar o novo código de cliente e servidor com o antigo, foram registradas dezenas de milhares de alterações de linha. Redesenhei o código de roteamento do servidor (como o acimahandleGet) Reescrevi todas as declarações de tipo para a API, fazendo muitas delas grandes mudanças estruturais. Além disso, reescrevi todas as partes do cliente nas quais as APIs alteradas foram chamadas. Durante este trabalho, 246 dos 292 arquivos de origem foram alterados.

Na maior parte deste trabalho, contei apenas com um sistema de tipos. Na última hora deste caso de 20 horas, comecei a executar testes que, na maioria das vezes, foram concluídos com êxito. No final, fizemos uma execução completa de testes e encontramos três pequenos erros.

Todos esses foram erros lógicos: condições que acidentalmente levaram o programa ao lugar errado. Normalmente, um sistema de tipos não ajuda a encontrar esses erros. Demorou alguns minutos para corrigir esses erros. Essa API reprojetada foi implantada há alguns meses atrás. Quando você lê algo sobrenosso site - é essa API que emite materiais relevantes.

Isso não significa que o sistema de tipo estático garante que o código esteja sempre correto. Este sistema não permite prescindir de testes. Mas isso simplifica bastante a refatoração.

Vou falar sobre a geração automática de código. Ou seja, usamos esquemas para gerar definições de tipo a partir da estrutura de nosso banco de dados. O sistema se conecta ao banco de dados do Postgres, analisa os tipos de coluna e grava as definições de tipo TypeScript correspondentes no arquivo regular .d.tsusado pelo aplicativo.

Um arquivo com tipos de esquema de banco de dados é atualizado por nosso script de migração toda vez que é iniciado. Devido a isso, não precisamos oferecer suporte manualmente a esses tipos. Os modelos usam definições de tipos de banco de dados para garantir que o código do aplicativo acesse corretamente tudo o que está armazenado no banco de dados. Não há tabelas ausentes, colunas ausentes ou entradas nullem colunas não suportadas null. Lembramos de processar corretamente nullnas colunas de suporte null. E tudo isso é verificado estaticamente no momento da compilação.

Tudo isso juntos cria uma cadeia de transferência de informações com tipo estatístico confiável, estendendo-se do banco de dados às propriedades dos componentes React no frontend:

  • , ( API) , .
  • API , API, ( ) .
  • React- , API, .

Enquanto trabalhava neste material, não consegui lembrar de um único caso de inconsistência no código associado à API que passou na compilação. Não tivemos falhas de produção devido ao fato de o código do cliente e do servidor relacionado à API ter idéias diferentes sobre o formulário de dados. E tudo isso não é resultado de testes automatizados. Nós, para a própria API, não escrevemos testes.

Isso nos coloca em uma posição extremamente agradável: podemos nos concentrar nas partes mais importantes da aplicação. Eu gasto muito pouco tempo fazendo conversões de tipo. Muito menos do que passei identificando as causas dos erros confusos que penetraram nas camadas de código escritas em Ruby ou JavaScript e causaram estranhas exceções em algum lugar muito longe da origem do erro.

É assim que o projeto cuida da tradução do back-end para o TypeScript. Como você pode ver, muito código foi escrito desde a transição. Tivemos tempo suficiente para avaliar as consequências da decisão.


O TypeScript é usado no front-end e no back-end do projeto, e

aqui não levantamos a pergunta usual para essas publicações, que é obter os mesmos resultados não através da digitação, mas através do uso de testes. Esses resultados não podem ser alcançados usando apenas testes. Nós, possivelmente, falaremos mais sobre isso.

Queridos leitores! Você traduziu projetos escritos em outros idiomas para o TypeScript?


All Articles