Migração de um código-fonte de um design system para Typescript: um diário de bordo
Sinopse
Este post é um breve relato de experiência da migração de um código-fonte de um design system de JavaScript para TypeScript. Portanto, entendemos que alguns pontos podem ser mais úteis para o seu projeto à medida que ele for mais parecido com o seu projeto.
O Confetti é um produto open source desenvolvido pela Labcodes e possui uma suíte de testes e pode ser encontrado no repositório a seguir: https://github.com/labcodes/confetti-ds . A sua documentação pode ser acessada no link a seguir: https://confetti.labcodes.com.br/.
É importante ressaltar que essa migração já estava prevista e alguns acontecimentos ajudaram a adiantar esse processo e como boas práticas ajudaram a guiar esse processo de desenvolvimento.
Contexto
Durante a publicação da versão 0.1.0-alpha.11
, fomos testar a versão do pacote dentro do terminal integrado do NPM, o RunKit, e foi verificado que a dependência do projeto @babel/runtime
não era encontrada. Além disso, já era conhecido que o Babel (transpilador de ES6 para common JS) estava causando um pouco de memory leak durante o processo de build.
Uma das causas prováveis para esses problemas eram a chamada direta ao CLI do Babel somados aos percalços dos empacotamentos e dependências; a consequência direta era o maior tamanho do bundle final.
Após tentar remover o pacote e revisar o processo de build, foi verificado que era uma boa hora para trocar o processo de build, e com essa migração para TS no futuro, já considerar o tsup como ferramenta de build.
Com cada componente possui o seu próprio arquivo, permitiu a equipe controlar melhor o funcionamento de cada um e testar paulatinamente cada alteração. A alta cobertura de testes colaborou positivamente na migração, já que o projeto possui uma boa cobertura e que permitia verificar cada passo da refatoração.
Como foi o processo?
Ajustando o build
Usamos o tsup como ferramenta de build com TS, que, por sua vez, usa ES Build por debaixo dos panos. O tsup possibilitou a equipe customizar e controlar melhor as etapas e parâmetros de formatação de código. O setup em si foi feito dentro de um arquivo novo chamado tsup.config.ts
, que permite selecionar e filtrar melhor os arquivos para configuração.
O resultado é um processo de bundle mais rápido, conforme descrito na documentação deles:
tsup automatically excludes packages specified in the dependencies and peerDependencies fields in the packages.json, but if it somehow doesn't exclude some packages, this library also has a special executable tsup-node that automatically skips bundling any Node.js package.
O estalar de dedos
Quando testamos o novo processo de build, verificamos que o produto comportava-se bem e não houveram bugs críticos e incompatibilidades com os projetos relacionados. Ao analisar o projeto como um todo e a disponibilidade da equipe para realizar uma migração, além do custo de tempo ser razoavelmente baixo, vimos que era o momento ideal para iniciar o processo de migração dos componentes de JS para TS.
Iniciando os trabalhos
Começamos a alterar as extensões dos arquivos de .js
para .tsx
. A extensão do Jest na IDE começou a sinalizar erros nos arquivos dos testes - o que de certa forma era esperado - e começamos a migrar também os arquivos de teste para .tsx
. Parte das mensagens de erro pode ser atribuída ao Typescript devido à sua natureza mais estrita.
Assim sendo, retomamos a migração para que fosse possível avançar nas refatorações sem tantos alertas. Para isso, removemos o ES Build (até porque o tsup + TS já estava realizando essa tarefa), e, ao mesmo tempo, desativamos o modo strict
do tsconfig, para nos dar mais segurança até os problemas serem resolvidos .
Precisávamos também exportar os tipos dos componentes através de um arquivo de declaração de tipos: types.d.ts
. Durante a compilação, os tipos são extraídos e exportados, podendo deixá-los disponíveis para API da pessoa desenvolvedora que queira usar a biblioteca do design system.
Cada componente teve a sua interface criada: no caso dos componentes base criamos a interface base para ser usada nos componentes derivados- voltaremos nessa parte logo mais, porque tivemos que realizar algumas mudanças nesse ponto também. Durante esse processo de declaração e estruturação das variáveis, também foram removidas as menções aos PropTypes, que se tornaram menos necessários agora que temos tipos para todos os componentes.
Pontos de atenção
Durante o processo, verificamos que alguns testes e ferramentas de trabalho ajudavam a localizar os problemas ao longo do processo. É importante ressaltar que o nível de conhecimento acerca do projeto pode influenciar mais do que o nível de senioridade, pois ajuda a entender a modelar as props
.
A seguir, seguem algumas dicas das anotações do processo de migração:
1. Muita atenção às props opcionais
Várias interfaces tinham props opcionais que foram marcadas como obrigatórias por acidente ao migrarmos de PropTypes para tipos, e isso causou vários erros. Levar em conta essa parte é essencial para validar a migração.
2. Usar interface
ao invés de type
Preferimos usar interfaces ao invés de types devido ao fato de permitir mais segurança para a pessoa usuária para customizar componentes pois são mais fáceis para estender e serem mais rigorosas na checagem de tipos.
3. Muita atenção na declaração dos tipos nos componentes
Como os componentes são funções, a diferença entre o tipo dos argumentos e os de retorno é um simples :
. No início do processo, era comum colocamos os tipos das props como retorno dos componentes.
4. Foque nos testes e crie configurações para rodá-los automaticamente
Como passamos a usar Turborepo no projeto, fazia com que a extensão Jest do VSCode passasse a não rodar os testes pois não encontrava o path correto do projeto.
5. Use as extensões corretas no VSCode (ou qualquer outra IDE de sua preferência).
No caso do VSCode, essas são nossas sugestões:
O Error Lens é uma extensão nativa da Microsoft que ajuda a verificar os erros de sintaxe do projeto. O Total Typescript foi desenvolvido por Matt Pollock, que possui diversos projetos de ensino de Typescript; já o Jest também trata-se de outra extensão nativa que automatizou bastante o processo de verificação e execução dos testes, que, certamente, foram também migrados para Typescript e realizamos um processo de double-check para verificar se as props estavam sendo renderizadas e usadas conforme o esperado (os links das extensões estão disponíveis na seção de Referências).
6. Passe um pente fino nos testes e documentações
Vale analisar se ainda há menções às declarações JS Docs (caso você as use), verificar se existem componentes sem menções às props na documentação; além disso, analisar se type-checking
estão impactando os testes.
7. Exporte todos os componentes para um único arquivo index
Ter apenas um arquivo de saída ajuda na geração de um arquivo index.d.ts
e na hora de importar os componentes no projeto que vai usar a biblioteca. Isso permite que uma outra pessoa usuária possa estender os tipos mais facilmente.
8. Para variantes de um mesmo componente, crie os tipos base e os estenda, mas só exporte os tipos de cada variante
Esse ponto é bem autodescritivo, como podemos ver no snippet a seguir, em que as propriedades base estão não dentro do componente abstract (o componente base), mas sim, dentro de uma interface ainda mais genérica.
Resultados
Após a migração, o tempo de build do pacote não teve grandes mudanças, mas agora há uma etapa adicional do processo, com a exportação dos tipos. O tamanho do código fonte exportado foi reduzido em cerca de 28% quando comparado ao build ainda em Javascript e Babel/ESLint.
Entretanto, o tamanho do do pacote, bem como o código em si, não sofreram alterações relevantes.
Antes da migração:
npm notice package size: 64.9 kB
npm notice unpacked size: 341.7 kB
Depois da migração:
npm notice package size: 78.5 kB (31kb de tipos, 47kb de código)
npm notice unpacked size: 365.5 kB
Aprendizados
O tamanho do do pacote, bem como o código em si, não sofreram alterações relevantes.
A migração em si, apesar dos seus percalços, foi bastante auxiliada pela grande cobertura dos testes; assim sendo, um código seguindo as boas práticas de TDD pode ajudar a entender melhor certas etapas do processo de alterações.
Além disso, o processo inicial foi realizado com strict: False
isto é, precisamos ativar para verdadeiro todas as validações de tipos estáticos. Ainda existem partes do código com tipo any
inferido implicitamente, por exemplo. Como foi mencionado no tópico 6 da seção de pontos de atenção.
1. Sempre comece os testes de dentro pra fora
Mesmo que os testes tenham alguns problemas de implementação, eles são a fonte de verdade da sua aplicação e vão mostrar imediatamente todos os problemas graves de tipos.
2. Tenha testes no seu projeto, por favor 🙏
Projetos com alta cobertura de testes são essenciais na DX para garantir melhorias e evoluções no código, principalmente quando se trata de uma refatoração tão grande.
3.Em caso de dúvida, procure migrar para Typescript o mais cedo possível
A migração em si não foi tão complicada, mas o esforço cresceria muito caso a biblioteca fosse maior, então esse é o tipo de decisão que deve ser tomada o mais cedo possível
4. Lembre de testar em um cenário real (nosso caso foi um projeto interno)
Os projetos que já faziam uso do DS não precisaram de muitas refatorações para atualizar o código para receber as atualizações do projeto.
Agradecimentos
I would like to thank my mentor Luciano Ratamero and one of the creators of the Confetti who agreed to help and without whom this migration would not have been possible and who co-authored this text.
Gostaria de agradecer ao meu mentor e um dos criadores do Confetti, Luciano Ratamero, que aceitou encarar o desafio da migração e co-autor deste texto.
Referências
https://github.com/labcodes/confetti-ds
https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html
https://www.youtube.com/watch?v=eh89VE3Mk5g
https://www.udemy.com/course/typescript-for-professionals/