Migrando um projeto JavaScript para TypeScript gradualmente

1. Por que migrar gradualmente? Estratégia e preparação

Migrar um projeto inteiro de JavaScript para TypeScript de uma só vez é uma tarefa assustadora e, na maioria dos casos, desnecessária. A abordagem incremental permite que sua equipe continue entregando valor enquanto adota os benefícios do TypeScript aos poucos. Os principais ganhos incluem: detecção precoce de erros, melhor documentação do código através de tipos, e um ambiente de desenvolvimento mais produtivo com autocompletar e refatorações seguras.

Antes de começar, avalie o tamanho do projeto, suas dependências e os riscos envolvidos. Projetos grandes com muitas dependências não tipadas exigirão mais trabalho de declaração de tipos. Comece configurando o ambiente com um tsconfig.json básico:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": false,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": false,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

As opções allowJs e checkJs são fundamentais: a primeira permite que arquivos JavaScript coexistam com TypeScript, enquanto a segunda ativa a verificação de tipos nos arquivos JS.

2. Primeiros passos: integrando TypeScript ao build existente

Instale o TypeScript como dependência de desenvolvimento:

npm install --save-dev typescript

Se você usa webpack, adicione o ts-loader ou babel-loader com preset TypeScript. Para Vite, o suporte a TypeScript é nativo. Exemplo com webpack:

// webpack.config.js
module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
};

Agora você pode começar a criar arquivos .ts que importam arquivos .js existentes. O compilador TypeScript entenderá ambos os formatos.

3. Ativando a verificação de tipos gradualmente

Uma estratégia eficaz é usar comentários de controle para ativar a verificação onde ela é mais útil. Adicione // @ts-check no topo de arquivos JS que você quer verificar, e // @ts-nocheck em arquivos que ainda não estão prontos:

// @ts-check
// src/utils/calculator.js
function add(a, b) {
  return a + b; // TypeScript agora infere os tipos
}

Configure checkJs: true no tsconfig.json e, em vez de ativar strict imediatamente, comece com strict: false. Isso permite que você veja erros sem ser sobrecarregado por todos eles de uma vez.

4. Convertendo arquivos JavaScript para TypeScript

Comece renomeando arquivos .js para .ts. Você provavelmente encontrará erros imediatos. A abordagem pragmática é usar any temporariamente:

// src/services/userService.ts
function getUser(id: any): any {
  // implementação existente
}

Para módulos sem declarações de tipo, use declare module:

// src/types/legacy-module.d.ts
declare module 'old-library' {
  export function doSomething(param: any): any;
}

5. Tipando o coração do projeto: funções, objetos e APIs

Refatore gradualmente funções críticas adicionando tipos aos parâmetros e retornos:

// src/services/paymentService.ts
interface PaymentPayload {
  amount: number;
  currency: string;
  customerId: string;
}

interface PaymentResult {
  success: boolean;
  transactionId?: string;
  error?: string;
}

async function processPayment(payload: PaymentPayload): Promise<PaymentResult> {
  const response = await fetch('/api/payments', {
    method: 'POST',
    body: JSON.stringify(payload),
  });
  return response.json();
}

Para callbacks e funções assíncronas, defina types específicos:

type Callback<T> = (error: Error | null, result?: T) => void;

function fetchData(url: string, callback: Callback<Data>) {
  // implementação
}

6. Lidando com dependências externas e código legado

Instale pacotes de tipos para bibliotecas populares:

npm install --save-dev @types/react @types/lodash @types/express

Para bibliotecas sem tipos, crie declarações customizadas em *.d.ts:

// src/types/global.d.ts
declare module 'untyped-library' {
  export function parse(input: string): Record<string, unknown>;
  export const VERSION: string;
}

Use unknown em vez de any quando possível, e aplique type guards para verificar tipos em tempo de execução:

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'name' in obj
  );
}

7. Avançando para configurações mais rigorosas

Quando a base de código estiver mais estável, ative strict: true no tsconfig.json. Isso habilita várias verificações rigorosas:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

Corrija erros comuns como variáveis potencialmente nulas:

function getLength(str: string | null): number {
  return str?.length ?? 0; // uso de optional chaining e nullish coalescing
}

8. Manutenção contínua e boas práticas

Configure um pipeline de CI que execute a verificação de tipos:

# .github/workflows/typecheck.yml
name: Type Check
on: [push, pull_request]
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npx tsc --noEmit

Para times, estabeleça code reviews focados em tipos. Monitore métricas como cobertura de tipos (porcentagem de arquivos .ts) e redução do uso de any. Ferramentas como typescript-coverage-report podem ajudar:

npx typescript-coverage-report

Lembre-se: a migração gradual é uma maratona, não uma corrida de velocidade. Cada arquivo convertido e cada tipo adicionado são passos em direção a um código mais seguro e sustentável.

Referências