Entendendo o Contexto: Por Que Migrar para TypeScript?
JavaScript é uma linguagem dinâmica e flexível, características que a tornaram popular, mas também geradora de bugs difíceis de rastrear em projetos grandes. TypeScript resolve esse problema ao adicionar verificação de tipos estática durante o desenvolvimento, permitindo que você capture erros antes da execução. Quando você trabalha em um projeto real com múltiplos desenvolvedores, refatorações frequentes ou código legado, essa segurança se torna essencial.
A migração não precisa ser um evento traumático onde você para tudo e reescreve o projeto. Projetos maduros podem fazer essa transição de forma gradual, permitindo que arquivos TypeScript coexistam com JavaScript. Essa estratégia reduz risco, mantém a produtividade e permite que o time aprenda TypeScript naturalmente. Nesta aula, você aprenderá exatamente como fazer isso em um projeto real.
Fase 1: Preparação e Configuração do Ambiente
Instalando TypeScript e Configurando o Projeto
Começamos adicionando TypeScript ao seu projeto. Você não precisa remover nada do JavaScript existente:
npm install --save-dev typescript
npx tsc --init
Isso cria um arquivo tsconfig.json que controla como o TypeScript compila seu código. Para um projeto em migração, configure-o de forma permissiva inicialmente:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Note que "strict": false inicialmente. Isso permite que arquivos TypeScript e JavaScript coexistam sem erros. Conforme ganhar confiança, você aumenta gradualmente o nível de rigor.
Integrando com seu Build System
Se você usa Webpack, Vite ou outro bundler, precise adicionar o loader TypeScript:
npm install --save-dev ts-loader
Para Webpack, atualize seu webpack.config.js:
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
};
Fase 2: Migração Gradual de Arquivos
Começando com Módulos Isolados
A chave para migração bem-sucedida é começar com arquivos que têm poucas dependências. Identifique módulos utilitários ou helpers que outras partes do código usam, mas que não dependem de muitos outros arquivos.
Vamos converter um arquivo de utilitários. Antes, em JavaScript puro:
// utils.js
export function formatCurrency(value) {
return '$' + (value / 100).toFixed(2);
}
export function calculateTax(amount, taxRate) {
return amount * (taxRate / 100);
}
export function parseDate(dateString) {
return new Date(dateString);
}
Migre para TypeScript adicionando tipos explícitos:
// utils.ts
export function formatCurrency(value: number): string {
return '$' + (value / 100).toFixed(2);
}
export function calculateTax(amount: number, taxRate: number): number {
return amount * (taxRate / 100);
}
export function parseDate(dateString: string): Date {
return new Date(dateString);
}
O compilador agora garante que formatCurrency recebe apenas números. Se alguém chamar formatCurrency("100"), o TypeScript avisará durante o desenvolvimento.
Importações Seguras em Código JavaScript
Seu código JavaScript ainda pode importar de módulos TypeScript sem problemas:
// calculator.js
import { formatCurrency, calculateTax } from './utils';
const total = calculateTax(1000, 10);
console.log(formatCurrency(total));
O TypeScript compila para JavaScript normal, então essa importação funciona perfeitamente.
Fase 3: Tipagem Avançada e Refatoração
Definindo Tipos Complexos
Conforme você migra mais arquivos, crie tipos reutilizáveis. Vamos imaginar uma aplicação de e-commerce:
// types.ts
export type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered';
export interface Product {
id: string;
name: string;
price: number;
quantity: number;
}
export interface Order {
id: string;
products: Product[];
status: OrderStatus;
createdAt: Date;
totalAmount: number;
}
export interface Customer {
id: string;
email: string;
name: string;
orders: Order[];
}
Agora qualquer arquivo que trabalhe com pedidos tem segurança de tipo:
// orderService.ts
import { Order, Product, OrderStatus } from './types';
export class OrderService {
calculateTotal(products: Product[]): number {
return products.reduce((sum, p) => sum + p.price * p.quantity, 0);
}
updateStatus(order: Order, newStatus: OrderStatus): Order {
return {
...order,
status: newStatus,
};
}
filterByStatus(orders: Order[], status: OrderStatus): Order[] {
return orders.filter(o => o.status === status);
}
}
Se um desenvolvedor tentar passar um status inválido, o TypeScript reclama imediatamente.
Genéricos para Reutilização
TypeScript oferece genéricos para criar funções e classes que funcionam com qualquer tipo, mas mantêm segurança:
// repository.ts
export class Repository<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: string, idField: keyof T): T | undefined {
return this.items.find(item => item[idField] === id);
}
getAll(): T[] {
return [...this.items];
}
remove(id: string, idField: keyof T): void {
this.items = this.items.filter(item => item[idField] !== id);
}
}
Reutilize essa classe para qualquer entidade:
// main.ts
import { Repository } from './repository';
import { Order, Customer } from './types';
const orderRepository = new Repository<Order>();
const customerRepository = new Repository<Customer>();
const order: Order = {
id: '123',
products: [],
status: 'pending',
createdAt: new Date(),
totalAmount: 0,
};
orderRepository.add(order);
const found = orderRepository.findById('123', 'id');
Fase 4: Tratamento de Dependências Externas
Tipando Bibliotecas sem Tipos
Nem todas as bibliotecas npm têm tipos TypeScript. Para as que não têm, você cria um arquivo de declaração .d.ts:
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
export function process(data: any): string;
export class Handler {
execute(input: string): Promise<void>;
}
}
Agora você pode usar a biblioteca com segurança de tipo:
// worker.ts
import { process, Handler } from 'legacy-lib';
const result: string = process(someData);
const handler = new Handler();
await handler.execute(result);
Consultando Tipos de Bibliotecas Modernas
Muitas bibliotecas modernas já incluem tipos. Se usar axios, por exemplo:
// api.ts
import axios, { AxiosResponse } from 'axios';
export interface ApiResponse<T> {
data: T;
status: number;
}
export async function fetchUser(id: string): Promise<ApiResponse<{ name: string; email: string }>> {
const response: AxiosResponse<{ name: string; email: string }> = await axios.get(
`/api/users/${id}`
);
return {
data: response.data,
status: response.status,
};
}
TypeScript automaticamente infere e valida que o tipo retornado corresponde ao esperado.
Fase 5: Aumentando o Rigor Gradualmente
Ativando Verificações Estritas
Depois que a maioria do código foi migrado, ative as verificações estritas no tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Isso obriga a tipagem explícita em todos os lugares. Inicialmente, você verá muitos erros — isso é esperado e produtivo. Resolva-os arquivo por arquivo.
Tratando Valores Nulos e Indefinidos
Uma vantagem crítica do strictNullChecks é forçar você a lidar explicitamente com valores que podem ser nulos:
// Sem strict: compila, mas pode quebrar em runtime
function getName(user) {
return user.name.toUpperCase();
}
// Com strict: força você a verificar
function getName(user: { name?: string }): string {
return user.name?.toUpperCase() ?? 'UNKNOWN';
}
Essa pequena mudança previne um dos bugs mais comuns em JavaScript.
Conclusão
A migração para TypeScript é um investimento que se paga rapidamente em projetos reais. Você aprendeu que a transição não precisa ser tudo ou nada — começar com módulos isolados, configurar o compilador permissivamente e aumentar gradualmente o rigor é a estratégia profissional. Os três ganhos principais são: erros capturados cedo durante o desenvolvimento, refatorações mais seguras porque o compilador valida e documentação viva através dos tipos, que nunca fica desatualizada como comentários tradicionais.