TypeScript 5.x em Produção: padrões e armadilhas que aprendi na prática
Trabalho com TypeScript desde 2017 — vi o ecossistema mudar completamente. Nos meus 10+ anos com Node e TypeScript na ThoughtWorks, Chipper Cash e agora na Haruo, aprendi padrões que funcionam e outros que parecem funcionar até o dia em que quebram em produção.
Este artigo não é sobre features básicas. É sobre padrões avançados do TS 5.x que uso diariamente e as armadilhas que todo time encontra no caminho.
1. Discriminated Unions para modelar estados
Esse é o padrão mais impactante que você pode adotar hoje. A ideia é simples: modele seus estados como uma união discriminada — cada variante carrega apenas os dados relevantes para aquele estado.
// ❌ Ruim: estado implícito com campos opcionais
type PaymentState = {
status: "pending" | "processing" | "completed" | "failed";
txId?: string;
error?: string;
completedAt?: Date;
};
Esse tipo permite estados impossíveis: { status: "completed", error: "timeout" } compila sem erro. Você precisa de verificações manuais em todo lugar.
// ✅ Bom: estados modelados como discriminated union
type PaymentState =
| { status: "pending" }
| { status: "processing"; txId: string }
| { status: "completed"; txId: string; completedAt: Date }
| { status: "failed"; error: PaymentError };
Com isso, o compilador garante que você nunca acesse error quando o status é “completed”, e nunca esqueça o txId no estado “processing”. O switch com exhaustive check fecha o círculo:
function handlePayment(state: PaymentState): string {
switch (state.status) {
case "pending": return "Aguardando...";
case "processing": return `Processando: ${state.txId}`;
case "completed": return `Concluído em ${state.completedAt}`;
case "failed": return `Erro: ${state.error.message}`;
}
}
Se você adicionar um novo status, o TypeScript avisa em todos os lugares que precisam tratar o novo caso.
Armadilha: Cuidado com discriminated unions muito grandes (8+ variantes). Elas podem desacelerar o type checker e ficam difíceis de navegar. Considere separar em tipos menores ou repensar o modelo.
2. Template Literal Types para APIs type-safe
Template literal types do TS 4.1+ são subestimados. Eles permitem criar tipos que representam strings com formato específico.
// Definindo rotas tipadas
type Route = `/api/${string}`;
type UserRoute = `/api/users/${number}`;
function get<T>(route: Route): Promise<T> { /* ... */ }
// ✅ Compila
get<User>("/api/users/42");
// ❌ Erro de tipo
get<User>("/users/42");
// ~~~~~~~~ "Argument of type '"/users/42"' is not assignable to parameter of type 'Route'"
Na Haruo, usamos esse padrão combinado com parâmetros de query:
type QueryRoute<R extends string> = `${R}?${string}`;
type ApiRoute<R extends string> = R | QueryRoute<R>;
// Uso real em um client HTTP tipado
const users = await api.get(`/api/users?page=1&limit=10` as const);
Outro padrão útil: extrair parâmetros de URLs em tempo de tipo:
type ExtractParams<T extends string> =
T extends `${string}:${infer P}/${infer Rest}`
? P | ExtractParams<Rest>
: T extends `${string}:${infer P}`
? P
: never;
type UserParams = ExtractParams<"/api/users/:id/posts/:postId">;
// Resultado: "id" | "postId"
Isso permite criar routers e clients totalmente type-safe sem código boilerplate.
3. O operador satisfies (e quando ele salva ou atrapalha)
Introduzido no TS 4.9, satisfies permite verificar se um valor satisfaz um tipo sem alterar seu tipo inferido.
// Sem satisfies: inferência perde precisão
const palette: Record<string, string | string[]> = {
red: "#ff0000", // tipo: string | string[]
blue: ["#0000ff"], // tipo: string | string[]
};
palette.red.toUpperCase(); // ❌ Erro: não existe em string | string[]
// Com satisfies: tipo inferido mantido, mas validado
const palette = {
red: "#ff0000", // tipo inferido: string
blue: ["#0000ff"], // tipo inferido: string[]
} satisfies Record<string, string | string[]>;
palette.red.toUpperCase(); // ✅ OK
palette.blue.map(c => c); // ✅ OK
Quando salva: Validação de configurações, objetos de rota, mapeamentos complexos onde você quer a liberdade do tipo inferido mas a garantia de conformidade.
Quando atrapalha: Combinado com tipos union complexos, satisfies pode desacelerar o editor drasticamente. Um caso que encontramos — um objeto de ~200 entradas de configuração com satisfies Record<string, ComplexConfig> aumentou o tempo de type check de 2s para 18s.
Regra prática: Use
satisfiespara objetos pequenos (< 50 entradas). Para configs grandes, prefira anotação de tipo direta comas constonde precisar de literais.
4. Branded Types para domínios ricos
CPF não é string. Email não é string. UserID não é string. No TypeScript, porém, todos são string. Branded types criam distinção:
type Brand<T, B> = T & { __brand: B };
type Cpf = Brand<string, "Cpf">;
type Email = Brand<string, "Email">;
type UserId = Brand<number, "UserId">;
function validateCpf(input: string): Cpf | null { /* ... */ }
function sendEmail(to: Email): void { /* ... */ }
const maybeCpf = validateCpf("123.456.789-09");
if (maybeCpf) {
// TypeScript não deixa passar string onde espera Cpf
sendEmail(maybeCpf); // ❌ Erro de tipo!
}
Isso previne uma categoria inteira de bugs: passar o argumento errado na ordem errada, confundir IDs de diferentes entidades, misturar dados de domínios distintos.
Na prática, usamos branded types com Zod para validação de entrada:
const CpfSchema = z.string().refine(
(s) => /^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(s),
"CPF inválido"
).transform((s) => s as Cpf);
// Safe: só criamos Cpfs validados
type Cpf = Brand<string, "Cpf">;
Cuidado: Branded types têm custo de complexidade. Não use para tudo — apenas para tipos que cruzam fronteiras do sistema (API boundaries, repositórios, eventos). Para uso interno no mesmo módulo, string simples resolve.
5. Type Performance: o que desacelera seu editor
TypeScript 5.x trouxe melhorias enormes de performance, mas alguns padrões ainda custam caro.
Os maiores vilões de performance:
1. Conditional types recursivos
// Lento: recursão profunda em tipos
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
Para objetos com 5+ níveis de profundidade, o type checker trava. Use DeepReadonly só onde realmente precisa.
2. Union types com 100+ membros
// Lento: enum de 200 valores
type Status = "a" | "b" | /* ... 198 mais */;
A cada arquivo que importa esse tipo, o TypeScript precisa avaliar a união inteira. Prefira enum ou const object para conjuntos grandes.
3. Template literal types com união em expansão
// MUITO lento: produto cartesiano de tipos
type AllRoutes = `${Method}_${Endpoint}`;
// Se Method tem 5 membros e Endpoint tem 100 = 500 combinações
4. satisfies com Record de objeto grande (Já mencionado — limite a ~50 entradas)
Diagnóstico rápido:
# Veja quanto tempo o TS passa em type checking
npx tsc --extendedDiagnostics
Procure pelo campo “I/O bound” vs “CPU bound”. Se for CPU bound e estiver > 5s, você tem tipos complexos demais.
6. Quando abrir mão de tipos
TypeScript é ferramenta, não religião. Existem momentos em que abrir mão de tipos estritos é a decisão correta:
Prototipação e experimentação
No início de um projeto, quando o modelo de dados ainda está mudando toda hora, tipos estritos atrapalham mais do que ajudam. Use any sabendo que vai refatorar depois.
Integração com bibliotecas mal tipadas
Algumas bibliotecas têm tipos quebrados ou inexistentes. Gastar horas fazendo tipos perfeitos para uma lib que você vai usar em 3 lugares não vale o esforço. // @ts-ignore com justificativa é aceitável.
Código gerado por IA
Se você usa vibe coding (veja nosso artigo sobre o tema) para gerar código descartável ou boilerplate, tipos perfeitos são secundários. Revise, tipifique o mínimo, e refatore quando o código se estabilizar.
Hotfix em produção
Quando um bug crítico está derrubando o sistema, a prioridade é restaurar o serviço. Tipos vêm depois. Mas não esqueça de voltar para tipificar corretamente após o incidente.
Conclusão
TypeScript 5.x é uma linguagem muito mais poderosa que o TypeScript de 2017. Discriminated unions, template literal types, satisfies e branded types são ferramentas que, usadas no lugar certo, eliminam categorias inteiras de bugs.
Mas toda ferramenta tem custo. Performance de type checking é um trade-off real. Saber quando não usar um padrão é tão importante quanto saber quando usá-lo.
Na Haruo, construímos sistemas que equilibram segurança de tipos com produtividade. Não somos dogmáticos — usamos tipos onde eles salvam, e abrimos mão onde eles atrapalham. O resultado é código que compila rápido, quebra raramente, e é fácil de manter.
Quer ajuda para evoluir a arquitetura TypeScript do seu projeto? Fale com a Haruo → (/#contato)
Quer levar isso para produção?
Na Haruo, implementamos agents de IA, automações e sistemas escaláveis para empresas que querem resultados reais. Vamos conversar sobre seu projeto.
Falar com a Haruo →