Introdução: Por que o código legado em TypeScript exige uma abordagem especial
Trabalhar com código legado é uma realidade inevitável para qualquer desenvolvedor, especialmente em projetos grandes. Muitas vezes, esse código foi escrito sem tipagem estrita, usando any em cada esquina, e representa um emaranhado confuso de dependências. Uma simples refatoração não funciona aqui — é necessária uma abordagem sistêmica e avançada.
Neste artigo, exploraremos estratégias de refatoração de código legado em TypeScript que vão além dos guias básicos. Você aprenderá como introduzir tipagem estrita com segurança, isolar mudanças usando padrões e aproveitar os recursos do compilador para automatizar tarefas rotineiras.
Estratégia 1: Introdução gradual de tipagem estrita através de "pontes de tipo"
Um dos principais problemas do código legado é o uso total de any. Tentar substituir todos os any por tipos específicos de uma só vez resultará em centenas de erros e paralisará o desenvolvimento. Em vez disso, usamos a técnica de "pontes de tipo".
A essência do método: não reescrevemos o módulo inteiro de uma vez, mas criamos tipos-pontes intermediários que gradualmente substituem o any.
Exemplo: Tipagem de um módulo "cinza"
Suponha que temos um módulo antigo para trabalhar com usuários:
// legacyModule.tsexport function processUser(data: any): any { // ... muito código ilegível return { id: data.id, name: data.name, age: data.age };}Em vez de tipar a função inteira imediatamente, criamos UserData e User:
// types.tsexport interface UserData { id: number; name: string; age?: number; // campo pode estar ausente}
export interface User { id: number; fullName: string; isAdult: boolean;}Agora usamos esses tipos, mas mantemos a assinatura antiga para compatibilidade reversa:
// legacyModule.ts (refatoração passo 1)import { UserData, User } from './types';
export function processUser(data: UserData): User { const isAdult = data.age ? data.age >= 18 : false; return { id: data.id, fullName: data.name, isAdult, };}
// Assinatura antiga para compatibilidadeexport function processUserLegacy(data: any): any { return processUser(data as UserData);}Depois que todos os consumidores migrarem para a nova função, removemos processUserLegacy. Essa abordagem minimiza o risco de quebrar dependências.
Estratégia 2: Isolamento de dependências "sujas" com o padrão "Adapter"
O código legado frequentemente está fortemente acoplado a bibliotecas externas, estados globais ou APIs obsoletas. Para realizar a refatoração com segurança, é necessário isolar essas dependências. A melhor ferramenta para isso é o padrão "Adapter" combinado com interfaces.
Exemplo: Isolamento de uma API obsoleta
// oldApi.ts (legado)export function fetchOldData(callback: (data: any) => void): void { // API antiga com callbacks setTimeout(() => callback({ value: 42 }), 100);}Criamos uma interface para a nova abordagem (baseada em Promise) e um adaptador:
// apiAdapter.tsimport { fetchOldData } from './oldApi';
export interface IDataFetcher { fetchData(): Promise<{ value: number }>;}
export class LegacyApiAdapter implements IDataFetcher { fetchData(): Promise<{ value: number }> { return new Promise((resolve, reject) => { fetchOldData((data) => { if (data) { resolve(data); } else { reject(new Error('No data')); } }); }); }}Agora todo o código novo usa IDataFetcher. Quando você reescrever completamente a API antiga, precisará apenas substituir a implementação do adaptador, sem tocar no restante do código. Isso reduz o risco de regressão ao mínimo.