
Estratégias de migração
Traduzir um projeto em grande escala de JavaScript para TypeScript é um desafio. Antes de começarmos a resolvê-lo, estudamos duas estratégias para mudar de JS para TS.
▍1. Estratégia de migração híbrida
Com essa abordagem, é realizada uma tradução gradual, arquivo por arquivo, do projeto para o TypeScript. Durante esse processo, os arquivos são editados, os erros de digitação corrigidos e funcionam assim até que todo o projeto seja traduzido para o TS. O parâmetro allowJS permite que você tenha arquivos TypeScript e arquivos JavaScript em seu projeto. Graças a isso, essa abordagem para traduzir projetos JS para TS é bastante viável.
Com uma estratégia de migração híbrida, você não precisa pausar o processo de desenvolvimento, você pode gradualmente, arquivo por arquivo, traduzir o projeto para TypeScript. Mas, se falamos de um projeto em grande escala, esse processo pode demorar muito. Também requer treinamento para programadores em toda a organização. Os programadores precisarão ser apresentados às especificações do projeto.
▍2. Estratégia de migração abrangente
Essa abordagem pega um projeto escrito inteiramente em JavaScript, ou uma parte do qual é escrito em TypeScript, e o transforma completamente em um projeto TypeScript. Neste caso, você precisará usar o tipo
any
e comentários @ts-ignore
, o que permitirá que o projeto seja compilado sem erros. Mas com o tempo, o código pode ser editado e passar a usar tipos mais adequados.
A estratégia de migração TypeScript abrangente tem várias vantagens significativas sobre a estratégia híbrida:
- . , , . , TypeScript, , .
- , . , , , . .
Diante do exposto, parece que a migração generalizada é superior à migração híbrida em todos os aspectos. Mas traduzir uma base de código madura para o TypeScript de uma maneira abrangente é uma tarefa muito difícil. Para resolvê-lo, decidimos recorrer a scripts para modificar o código, os chamados "codemods" ( codemods ). Quando começamos a traduzir um projeto para TypeScript, fazendo isso manualmente, notamos operações repetitivas que poderiam ser automatizadas. Escrevemos mods de código para cada uma dessas operações e os combinamos em um único pipeline de migração.
A experiência nos diz que não podemos ter 100% de certeza de que, após a tradução automática de um projeto para o TypeScript, não haverá erros. Mas descobrimos que a combinação de etapas descritas a seguir nos deu os melhores resultados e, no final, conseguimos um projeto TypeScript sem erros. Usando mods de código, fomos capazes de traduzir para o TypeScript um projeto contendo mais de 50.000 linhas de código e representado por mais de 1.000 arquivos. Demorou um dia para fazer isso.
Com base no pipeline mostrado na figura a seguir, criamos a ferramenta ts-migrate.

Codemods Ts-migrate do
Airbnb tem uma grande parte de seu frontend escrito usando React . É por isso que algumas partes do mod de código estão relacionadas a conceitos específicos do React. A ferramenta ts-migrate pode ser usada com outras bibliotecas ou estruturas, mas isso exigirá configuração e testes adicionais.
Visão geral do processo de migração
Vamos percorrer as etapas básicas que você precisa seguir para traduzir um projeto de JavaScript para TypeScript. Vamos falar sobre como essas etapas são implementadas.
▍Passo 1
A primeira coisa que todo projeto TypeScript cria é um
tsconfig.json
. O Ts-migrate pode fazer isso sozinho, se necessário. Existe um modelo padrão para este arquivo. Além disso, um sistema de verificação está em vigor para garantir que todos os projetos sejam configurados de forma consistente. Aqui está um exemplo de configuração básica:
{
"extends": "../typescript/tsconfig.base.json",
"include": [".", "../typescript/types"]
}
▍Passo 2
Assim que o arquivo
tsconfig.json
estiver onde deveria estar, os arquivos de origem são renomeados. Ou seja, as extensões .js / .jsx mudam para .ts / .tsx. Esta etapa é muito fácil de automatizar. Isso permite que você se livre de muito trabalho manual.
▍Passo 3
E agora é hora de iniciar os mods de código! Nós os chamamos de plug-ins. Plug-ins para ts-migrate são mods de código que têm acesso a informações adicionais por meio do servidor de linguagem TypeScript. Os plug-ins aceitam strings como entrada e retornam strings modificadas. A caixa de ferramentas jscodeshift , API TypeScript, ferramentas de processamento de string ou outras ferramentas de modificação AST podem ser usadas para realizar transformações de código .
Depois de concluir cada uma das etapas acima, verificamos se há alguma mudança pendente no histórico do Git e as incluímos no projeto. Isso permite que você divida os PRs de migração em commits, o que torna mais fácil entender o que está acontecendo e ajuda a rastrear as mudanças nos nomes dos arquivos.
Visão geral dos pacotes que constituem o ts-migrate
Dividimos o ts-migrate em 3 pacotes:
Fazendo isso, fomos capazes de separar a lógica de transformação do código do núcleo do sistema e fomos capazes de criar muitas configurações projetadas para resolver diferentes problemas. Agora temos duas configurações principais: migração e reignore .
O objetivo de aplicar a configuração
migration
é traduzir o projeto de JavaScript para TypeScript. E a configuração reignore
é usada para tornar possível a compilação do projeto simplesmente ignorando os erros. Essa configuração é útil quando você tem uma grande base de código e faz várias coisas com ela, como o seguinte:
- Atualização da versão do TypeScript.
- Fazer mudanças importantes no código ou refatorar a base de código.
- Tipos aprimorados de algumas bibliotecas comumente usadas.
Com essa abordagem, podemos traduzir o projeto para TypeScript mesmo se, durante a compilação, forem gerados erros com os quais não planejamos lidar imediatamente. Também torna mais fácil atualizar o TypeScript ou as bibliotecas usadas em seu código.
Ambas as configurações são executadas em um servidor
ts-migrate-server
que tem duas partes:
- TSServer : esta parte do servidor é muito semelhante ao que o VSCode usa para se comunicar entre o editor e o servidor de linguagem. A nova instância do servidor da linguagem TypeScript começa em um processo separado. As ferramentas de desenvolvimento interagem com ele usando um protocolo de linguagem .
- Ferramenta de migração : este é o código que executa o processo de migração e coordena esse processo. Essa ferramenta leva os seguintes parâmetros:
interface MigrateParams {
rootDir: string; // .
config: MigrateConfig; // ,
// .
server: TSServer; // TSServer.
}
Esta ferramenta faz o seguinte:
- Analisando o arquivo
tsconfig.json
. - Gerando arquivos .ts com código-fonte.
- Envie cada arquivo para o servidor de linguagem TypeScript para diagnosticar esse arquivo. Existem três tipos de diagnósticos, que nos dá o compilador:
semanticDiagnostics
,syntacticDiagnostics
esuggestionDiagnostics
. Usamos essas verificações para encontrar áreas problemáticas no código-fonte. Com base no código de diagnóstico exclusivo e no número da linha no arquivo, podemos identificar o possível tipo de problema e aplicar as modificações de código necessárias. - Processando cada arquivo por todos os plug-ins. Se o texto no arquivo foi alterado por iniciativa do plugin, atualizamos o conteúdo do arquivo original e notificamos o servidor de idioma que o arquivo foi alterado.
Os exemplos de uso
ts-migrate-server
podem ser encontrados no pacote de exemplos ou no pacote principal . Ele ts-migrate-example
também contém exemplos básicos de plug-ins . Eles se enquadram em 3 categorias principais:
- Plugins baseados em jscodeshift.
- Plug-ins baseados no TypeScript da árvore de sintaxe abstrata (AST).
- Plug - ins de processamento de texto.
O repositório contém um conjunto de exemplos destinados a demonstrar o processo de criação de plug-ins simples de todos esses tipos. Também mostra seu uso em combinação c
ts-migrate-server
. Aqui está um exemplo de pipeline de migração que transforma o código. O seguinte código é recebido em sua entrada:
function mult(first, second) {
return first * second;
}
E ele dá o seguinte:
function tlum(tsrif: number, dnoces: number): number {
console.log(`args: ${arguments}`);
return tsrif * dnoces;
}
Neste exemplo, ts-migrate executou 3 transformações:
- Ele inverte a ordem dos caracteres em todos os identificadores:
first -> tsrif
. - Adicionadas informações sobre os tipos na declaração de função:
function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number
. - Adicionou a linha ao código
console.log(‘args:${arguments}’);
Plugins de uso geral
Os plug-ins reais estão localizados em um pacote separado - ts-migrate-plugins . Vamos dar uma olhada em alguns deles. Temos dois plug-ins baseados em jscodeshift:
explicitAnyPlugin
e declareMissingClassPropertiesPlugin
. O kit de ferramentas jscodeshift permite converter ASTs em código normal usando o pacote reformulado . Podemos usar a função toSource()
para atualizar diretamente o código-fonte contido em nossos arquivos.
O plug-in explicitAnyPlugin recupera informações do servidor da linguagem TypeScript sobre todos os erros
semanticDiagnostics
e as linhas em que esses erros foram detectados. Em seguida, a anotação de tipo é adicionada a essas linhas any
. Esta abordagem permite que você corrija erros, já que usar o tipoany
permite que você se livre de erros de compilação.
Aqui estão alguns exemplos de código antes do processamento:
const fn2 = function(p3, p4) {}
const var1 = [];
Aqui está o mesmo código processado pelo plugin:
const fn2 = function(p3: any, p4: any) {}
const var1: any = [];
O declareMissingClassPropertiesPlugin pega todas as mensagens de diagnóstico com um código de erro
2339
(você consegue adivinhar o que esse código significa ?) E, se ele pode encontrar declarações de classe com identificadores ausentes, adiciona-as ao corpo da classe anotada any
. A partir do nome do plugin, podemos concluir que ele é aplicável apenas às classes ES6 .
A próxima categoria de plug-ins é baseada em AST TypeScript. Ao processar o AST, podemos gerar uma série de atualizações a serem feitas no arquivo de origem. As descrições dessas atualizações são assim:
type Insert = { kind: 'insert'; index: number; text: string };
type Replace = { kind: 'replace'; index: number; length: number; text: string };
type Delete = { kind: 'delete'; index: number; length: number };
Depois de gerar informações sobre as atualizações necessárias, resta apenas inseri-las no arquivo na ordem inversa. Se, após realizar esta operação, recebermos um novo código de programa, atualizaremos o arquivo do código-fonte em conformidade.
Vamos dar uma olhada nos próximos plug-ins baseados em AST. Este é
stripTSIgnorePlugin
e hoistClassStaticsPlugin
.
O plugin stripTSIgnorePlugin é o primeiro plugin usado no pipeline de migração. Ele remove todos os comentários do arquivo.
@ts-ignore
(esses comentários nos permitem dizer ao compilador para ignorar os erros que ocorrem na próxima linha). Se estivermos traduzindo um projeto escrito em JavaScript para TypeScript, este plug-in não executará nenhuma ação. Mas se estivermos falando de um projeto parcialmente escrito em JS e parcialmente em TS (vários de nossos projetos estavam em um estado semelhante), então esta é a primeira etapa de migração que não pode ser dispensada. Somente depois que os comentários forem removidos @ts-ignore
, o compilador TypeScript gerará mensagens de erro de diagnóstico que precisam ser corrigidas.
Aqui está o código que entra na entrada deste plugin:
const str3 = foo
? // @ts-ignore
// @ts-ignore comment
bar
: baz;
Aqui está o resultado:
const str3 = foo
? bar
: baz;
Depois de nos livrar dos comentários,
@ts-ignore
executamos o plug- in hoistClassStaticsPlugin . Ele passa por todas as declarações de classe. O plugin detecta a possibilidade de levantar identificadores ou expressões e descobre se uma determinada operação de atribuição já foi elevada ao nível de classe.
Para garantir a alta velocidade de desenvolvimento e evitar downgrades forçados para versões anteriores do projeto, fornecemos cada plugin e ts-migrate com um conjunto de testes de unidade.
Plug-ins relacionados ao React
O plugin reactPropsPlugin , baseado nesta grande ferramenta, converte informações de tipo de PropTypes em declarações de tipo TypeScript. Com este plugin, você só precisa processar arquivos .tsx que contenham pelo menos um componente React. Este plugin procura todas as declarações PropTypes e tenta analisá-las usando ASTs e expressões regulares simples como
/number/
, ou usando expressões regulares mais complexas como / objectOf $ / . Quando é detectado Reagir-componente (função ou com base na classe), é transformado em um componente em que um novo tipo é usado para parâmetros de entrada (suportes): type Props = {…};
. Plug-
in ReactDefaultPropsPluginé responsável por implementar o padrão defaultProps nos componentes React . Usamos um tipo especial para representar os parâmetros de entrada que recebem valores padrão:
type Defined<T> = T extends undefined ? never : T;
type WithDefaultProps<P, DP extends Partial<P>> = Omit<P, keyof DP> & {
[K in Extract<keyof DP, keyof P>]:
DP[K] extends Defined<P[K]>
? Defined<P[K]>
: Defined<P[K]> | DP[K];
};
Tentamos encontrar os adereços aos quais foram atribuídos valores padrão e, em seguida, combiná-los com o tipo que descreve os adereços para o componente que criamos na etapa anterior.
O ecossistema React faz uso extensivo dos conceitos de ciclo de vida de estado e componente. Enfrentaremos os desafios relacionados a esses conceitos nos próximos plug-ins. Portanto, se o componente tiver um estado, o plug- in reactClassStatePlugin gera um novo tipo (
type State = any;
) e o plug- in reactClassLifecycleMethodsPlugin anota os métodos de ciclo de vida do componente com os tipos correspondentes. A funcionalidade desses plug-ins pode ser expandida, inclusive equipando-os com a capacidade de substituí-los por any
tipos mais precisos.
Esses plug-ins podem ser aprimorados, em particular, estendendo o suporte de tipo para estado e propriedades. Mas seus recursos existentes, como se viu, são um bom ponto de partida para implementar a funcionalidade de que precisamos. Também não trabalhamos com ganchos React aqui , porque no início da migração, nossa base de código usava uma versão antiga do React que não suporta ganchos.
Verificar se o projeto está compilado corretamente
Nosso objetivo é compilar um projeto TypeScript equipado com tipos básicos sem alterar o comportamento do programa.
Depois de todas as transformações e modificações, nosso código pode acabar não sendo formatado de maneira uniforme, o que pode levar ao fato de que algumas verificações de código com o linter revelam erros. Nosso codebase frontend usa um sistema baseado em Prettier e ESLint. Ou seja, o Prettier é usado para formatação automática de código e o ESLint ajuda a verificar se o código está em conformidade com as abordagens de desenvolvimento recomendadas. Tudo isso nos permite lidar rapidamente com problemas de formatação de código decorrentes de ações anteriores, simplesmente usando o plugin apropriado .-
eslintFixPlugin
.
A etapa final no pipeline de migração é verificar se todos os problemas de compilação do TypeScript foram resolvidos. Para localizar e corrigir possíveis erros, o plug - in tsIgnorePlugin obtém informações do diagnóstico semântico do código e dos números de linha e, em seguida, adiciona comentários ao código
@ts-ignore
com explicações dos erros. Por exemplo, pode ser assim:
// @ts-ignore ts-migrate(7053) FIXME: No index signature with a parameter of type 'string...
const { field1, field2, field3 } = DATA[prop];
// @ts-ignore ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const field2 = object.some_property;
Equipamos o sistema com suporte à sintaxe JSX:
{*
// @ts-ignore ts-migrate(2339) FIXME: Property 'NORMAL' does not exist on type 'typeof W... */}
<Text weight={WEIGHT.NORMAL}>
some text
</Text>
<input
id="input"
// @ts-ignore ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'string'.
name={getName()}
/>
Ter mensagens de erro significativas à nossa disposição facilita a correção de erros e a localização de snippets de código a serem observados. Comentários relevantes, em combinação com
$TSFixMe
, nos permitem coletar dados valiosos sobre a qualidade do código e encontrar fragmentos de código potencialmente problemáticos. $TSFixMe
É o alias de tipo que criamos any
. E para funções, é isso $TSFixMeFunction = (…args: any[]) => any;
. É recomendável evitar o uso de um tipo any
, mas usá-lo nos ajudou a simplificar o processo de migração. O uso desse tipo nos ajudou a saber exatamente quais fragmentos de código precisavam ser melhorados.
É importante notar que o plugin
eslintFixPlugin
é executado duas vezes. Primeira vez antes de usartsIgnorePlugin
já que a formatação pode afetar as mensagens sobre onde ocorrem os erros de compilação. A segunda vez é após a aplicação tsIgnorePlugin
, pois adicionar comentários ao código @ts-ignore
pode levar a erros de formatação.
Notas Adicionais
Gostaríamos de chamar sua atenção para alguns recursos de migração que observamos durante o trabalho. Talvez saber sobre esses recursos seja útil ao trabalhar com seus projetos.
- TypeScript 3.7 @ts-nocheck, TypeScript- . , .js-, .ts/.tsx-. , .
- TypeScript 3.9 introduz suporte para comentários @ ts-expect-error . Se uma linha de código for prefixada com esse comentário, o TypeScript não relatará o erro correspondente. Se não houver erro em tal linha, o TypeScript informará
@ts-expect-error
que não há necessidade de comentários . A base de código do Airbnb mudou de comentários@ts-ignore
para comentários@ts-expect-error
.
Resultado
A migração da base de código do Airbnb de JavaScript para TypeScript ainda está em andamento. Temos alguns projetos antigos que ainda são representados por código JavaScript.
$TSFixMe
Os comentários ainda são comuns em nossa base de código @ts-ignore
.

JavaScript e TypeScript no Airbnb
Mas deve-se notar que usar ts-migrate acelerou muito o processo de tradução de nossos projetos de JS para TS e melhorou muito a produtividade do nosso trabalho. Com ts-migrate, os programadores puderam se concentrar em melhorar a digitação, em vez de processar manualmente cada arquivo. Atualmente, aproximadamente 86% de nosso mono-repositório front-end, que tem cerca de 6 milhões de linhas de código, é traduzido para TypeScript. Esperamos chegar a 95% até o final deste ano.
Aqui na página inicial do repositório do projeto, você pode aprender como instalar e executar ts-migrate. Se você encontrar algum problema no ts-migrate, ou se tiver ideias para melhorar esta ferramenta, nós o convidamos a participar.para trabalhar nisso!
Você já traduziu grandes projetos de JavaScript para TypeScript?
