Material 9 · Design para páginas e campanhas
TypeScript: o contrato que o código assina antes de ir ao ar
Todo formulário de lead, painel de métricas e botão de envio carrega uma promessa: o campo que o código lê existe, o número é número e o estado de erro foi tratado. TypeScript é a forma de escrever essa promessa em cima do JavaScript e deixar um programa conferir antes de cada publicação. Este guia junta o que mudou até novembro de 2026 (o 6.0 que limpou o tsconfig, o 7.0 nativo em Go, o Node rodando .ts sem build) com 34 capítulos, do primeiro tipo à API com tipo de ponta a ponta, e cada exemplo dá para abrir num laboratório com o compilador de verdade.
Pesquisa e redação feitas entre 11 e 12 de novembro de 2026 por sete modelos do geo-orchestrator (Perplexity, GPT-6 Astra, Gemini 4 Argon, Claude Fable 5.1, Claude Opus 5.5 e GPT-6.1 Sol), com as versões conferidas no registro do npm no mesmo dia. Os 101 blocos marcados com a faixa azul e os 60 padrões do catálogo foram compilados um a um com o TypeScript 6.0.3 antes de publicar: o que aparece em “o compilador respondeu” é a saída real. A agência Mirante e os clientes dos exemplos são fictícios.
- 7.0.2versão estável no npm, nativa em Go, desde 8 de julho
- 11,9×mais rápido no build do VS Code (de 125,7 s para 10,6 s)
- 22.18primeiro Node.js que roda
node app.tssem flag - 101exemplos conferidos no compilador real, mais 60 padrões no catálogo
Bancada: cole um JSON, receba o tipo e o esquema
O jeito mais rápido de entender TypeScript é ver um dado real virar contrato. Cole a resposta de uma API, o corpo de um webhook ou o lead que o formulário envia. A bancada lê o exemplo, reconhece e-mail, URL, UUID e data, marca como opcional o campo que falta em parte da lista e escreve o tipo, a guarda manual e o mesmo contrato em Zod 4, Valibot 1.5 e ArkType 2.2. O botão “Conferir no compilador real” manda tudo para o laboratório e prova que o exemplo cabe no tipo gerado.
Carregando a bancada…
// Ative o JavaScript para gerar o tipo a partir do JSON.
export interface Lead {
nome: string;
email: string;
orcamentoEmCentavos: number;
}A inferência olha um exemplo só. Um campo que veio null uma vez pode ser texto em outra; uma lista vazia vira unknown[]. Por isso o esquema (Zod, Valibot, ArkType) é o que protege a produção: ele confere cada dado que chega, e o tipo sai dele com z.infer.
O tipo confere antes de ir ao ar e some depois
TypeScript é JavaScript com anotações de tipo: pequenas marcações que dizem o formato de cada valor (number, string, um objeto Lead). O compilador (o programa tsc, que lê o código e confere essas marcações) aponta o que não bate e depois apaga tudo. O que vai para o navegador é JavaScript comum, sem nenhum tipo.
Para quem monta landing page, formulário de captura e painel de métricas, isso muda o momento em que o erro aparece. Sem tipos, ele aparece quando o cliente reclama que o lead não chegou. Com tipos, aparece sublinhado em vermelho no editor, antes do deploy.
O erro clássico do orçamento
Todo valor que sai de um <input> chega como texto, mesmo quando o campo é numérico. Em JavaScript puro, uma função que calcula orçamento em centavos aceita esse texto sem reclamar. O operador + vira concatenação (junção de textos), e 5000 * 30 + "15000" resulta em "15000015000" no lugar de 165000. O painel mostra um orçamento de cento e cinquenta milhões de reais, e ninguém percebe até a reunião.
Com a função tipada, o compilador recusa a chamada:
// Orçamento em centavos: verba diária vezes dias, mais a taxa da Mirante
function calcularOrcamento(verbaDiaria: number, dias: number, taxa: number): number {
return verbaDiaria * dias + taxa;
}
// Valor lido de um input do formulário: sempre chega como texto
const taxaDoFormulario = "15000";
// erro esperado: TS2345
const total = calcularOrcamento(5000, 30, taxaDoFormulario);
// Convertendo antes, a chamada passa
const corrigido = calcularOrcamento(5000, 30, Number(taxaDoFormulario));
// ^?- linha 13
const corrigido: number - linha 10
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
O TS2345 diz que um argumento do tipo string não cabe num parâmetro do tipo number. A correção é explícita: converter com Number() e decidir o que fazer se a conversão falhar.
Campo com nome errado e undefined no meio do funil
Os outros dois tropeços mais comuns em código de captura são digitar o nome do campo errado e esquecer que um campo é opcional. Com strict ligado (o modo rigoroso, padrão desde o TypeScript 6.0), os dois aparecem na hora:
type Lead = {
nome: string;
email: string;
telefone?: string; // opcional no formulário
};
function enviarBoasVindas(lead: Lead): string {
// erro esperado: TS2551
const destino = lead.emial;
// erro esperado: TS18048
const ddd = lead.telefone.slice(0, 2);
return `${destino} ${ddd}`;
}- linha 9
TS2551: Property 'emial' does not exist on type 'Lead'. Did you mean 'email'? - linha 11
TS18048: 'lead.telefone' is possibly 'undefined'.
O TS2551 ainda sugere o nome certo (email). O TS18048 avisa que telefone pode ser undefined, e obriga você a tratar o lead que não preencheu o campo.
O que fica fora do alcance
O compilador só enxerga o código que você escreveu. Ele não sabe o que vai chegar pela rede: o corpo de um webhook (aviso automático que outro sistema envia para a sua URL), a resposta de uma API de anúncios, o JSON de um formulário embutido em outro site. Quando você declara que a resposta é um Lead, o TypeScript acredita, mesmo que o servidor mande outra coisa.
Para esses dados é preciso validação em tempo de execução: código que confere o valor de verdade quando ele chega. Bibliotecas como Zod, Valibot e ArkType fazem isso e ainda geram o tipo a partir da validação, então tipo e checagem ficam sempre iguais.
| Situação | TypeScript pega? | O que usar |
|---|---|---|
| Nome de campo digitado errado | Sim | Tipo do objeto |
| Texto passado onde se espera número | Sim | Anotação do parâmetro |
| Campo opcional usado sem checagem | Sim, com strict | Narrowing (checar antes de usar) |
| Payload de webhook com formato diferente | Não | Zod, Valibot ou ArkType |
| E-mail com formato inválido | Não | Validação em tempo de execução |
Onde o tipo existe
O tipo vive em três lugares: no editor (autocompletar, sublinhado de erro, renomear com segurança), no compilador e no passo de build (a etapa que transforma o projeto no que vai ao ar). No JavaScript final ele já foi apagado. Por isso TypeScript não deixa a página mais lenta nem aumenta o peso do bundle.
O Node.js atual vai além e executa arquivos .ts direto, apagando os tipos na hora (type stripping). Ele não verifica nada nesse processo: a conferência continua sendo trabalho do tsc.
# confere os tipos sem gerar arquivos
npx tsc --noEmitQuanto custa adotar
O custo inicial é um tsconfig.json (arquivo de configuração do compilador), algumas horas para o time se acostumar com as mensagens de erro e o hábito de declarar o formato dos dados principais: lead, campanha, métrica. O custo de espera caiu muito: o TypeScript 7, com compilador nativo escrito em Go, compilou o projeto do VS Code em 10,6 segundos, contra 125,7 da versão anterior.
Quando não compensa:
- Script de 20 linhas que dispara um pixel de conversão e nunca mais muda.
- Protótipo descartável para mostrar ao cliente numa tarde.
- Snippet colado no gerenciador de tags, onde não existe passo de build.
Quando compensa: qualquer código que vai durar mais de um sprint, que mais de uma pessoa mexe ou que mexe com dinheiro (orçamento, comissão, verba de mídia). Na Mirante, o critério prático é este: se o código recebe um lead ou soma centavos, ele é tipado.
Do 5.x ao 7.0: o que muda no seu tsconfig em 2026
Linha do tempo das versões
Em 2026 o TypeScript passou por duas versões grandes. O 6.0 é uma versão de transição: ele avisa sobre o que vai sumir. O 7.0 é o compilador (programa que verifica os tipos e gera JavaScript) reescrito do zero. Para chegar lá sem susto, ajuda saber o que cada versão trouxe desde a 5.4.
| Versão | Data | O que trouxe |
|---|---|---|
| 5.4 | — | NoInfer, narrowing preservado em closures após a última atribuição, module preserve |
| 5.5 | — | predicados de tipo inferidos, isolatedDeclarations, verificação de sintaxe de regex, ${configDir} |
| 5.6 | — | aviso de condições sempre verdadeiras, noUncheckedSideEffectImports, noCheck |
| 5.7 | — | rewriteRelativeImportExtensions, aviso de variável nunca inicializada, es2024 |
| 5.8 | — | erasableSyntaxOnly, module node18, checagem por ramo em return condicional |
| 5.9 | — | import defer, module node20 |
| 6.0 | 23/03/2026 | novos padrões, descontinuações, es2025, imports com #/ |
| 6.0.3 | 16/04/2026 | última versão do compilador escrito em JavaScript |
| 7.0 | 08/07/2026 | compilador nativo em Go, com paralelismo (7.0.2 é o latest no npm) |
| 7.1 beta | 20/10/2026 | nova API, import source, target/lib es2026 (Math.sumPrecise) |
O narrowing citado na tabela é o estreitamento de tipo, quando o compilador deduz um tipo mais específico depois de um if.
Os novos padrões do 6.0
Vários valores padrão mudaram no 6.0. Um projeto da Mirante que nunca mexeu no tsconfig pode começar a mostrar erros sem que ninguém tenha alterado uma linha de código.
strictvem ligado: o modo estrito (conjunto de checagens mais rigorosas) passa a valer sem você pedir.moduleéesnextetargetées2025: o código sai em JavaScript moderno.typesé[]: os pacotes@typesnão entram mais sozinhos, então liste"node"e o que mais usar.rootDirpassa a ser o diretório do próprio tsconfig.noUncheckedSideEffectImportsvem ligado: umimport "./estilos.css"que não resolve vira erro.libReplacementvem desligado.
O que o 6.0 descontinuou
As opções abaixo geram aviso no 6.0 e foram removidas no 7.0:
target: es5edownlevelIterationmoduleResolutionnode(ounode10) eclassicmoduleamd,umd,systemenonebaseUrleoutFilealwaysStrict: false- a palavra
modulepara declarar namespaces (usenamespace) - import assertions com
assert(usewith) - a diretiva
no-default-lib
Na prática, a maioria dos projetos de agência esbarra em quatro delas: baseUrl, moduleResolution: node, target: es5 e outFile. O bloco abaixo mostra um tsconfig antigo de painel de métricas, com as linhas que saem comentadas e as que entram no lugar.
{
"compilerOptions": {
// sai: "target": "es5"
"target": "es2025",
// sai: "module": "amd"
"module": "esnext",
// sai: "moduleResolution": "node"
// front com Vite ou Next: bundler; back em Node puro: nodenext
"moduleResolution": "bundler",
// sai: "baseUrl": "."
// os caminhos de paths passam a ser relativos ao tsconfig
"paths": { "@/*": ["./src/*"] },
// sai: "outFile": "dist/painel.js"
// quem junta os arquivos agora é o bundler; o tsc escreve em uma pasta
"outDir": "dist",
// os @types não entram mais sozinhos
"types": ["node"]
}
}Use bundler quando um empacotador (bundler, como Vite ou esbuild) resolve os imports. Use nodenext (com "module": "nodenext") em serviços que o Node executa direto, como o receptor de webhooks da Mirante. Se preferir abandonar paths, os imports com #/, declarados no campo imports do package.json, são outra saída.
O 7.0 nativo em Go
O 7.0 é o Project Corsa: o compilador reescrito em Go, uma linguagem compilada para código de máquina, que roda várias verificações em paralelo. A Microsoft mediu o build do VS Code: ele caiu de 125,7 s para 10,6 s, ou seja, ficou 11,9 vezes mais rápido. Outros projetos medidos ganharam entre 7,7 e 8,9 vezes.
Para o seu dia a dia, pouca coisa muda. O pacote continua sendo typescript e o comando continua sendo tsc. O nome tsgo e o pacote @typescript/native-preview eram só da fase de prévia. A emissão de .d.ts (arquivos de declaração, que descrevem os tipos de uma biblioteca) funciona normalmente.
Ainda faltam algumas peças:
- API programática estável: ferramentas que importam o compilador como biblioteca, como alguns linters e geradores de código, podem quebrar. A nova API chega com o 7.1, hoje em beta.
- Plugins de linguagem: o suporte a Vue, Svelte e, em parte, Angular depende desses plugins e pode exigir o 6.0.
- Navegador: o 7 não roda no navegador. Os playgrounds online usam o compilador JavaScript 6.0.3.
A saída é instalar as duas versões lado a lado com um alias do npm (um apelido que instala um pacote com outro nome):
# o 7.0 fica como "typescript" e fornece o comando tsc
npm install -D typescript@7
# o 6.0 entra com outro nome, para ferramentas que usam a API
npm install -D typescript-6@npm:typescript@6
# verificação rápida com o 7
npx tsc --noEmit
# verificação com o 6, se precisar comparar resultados
node node_modules/typescript-6/bin/tsc --noEmitNo lint, o Oxlint com --type-aware já usa o motor do TypeScript 7. Já o typescript-eslint depende da API e pode precisar do 6.0 instalado ao lado.
Roteiro de atualização em 5 passos
- Suba para o 6.0 com prazo extra. Instale
typescript@6e acrescente"ignoreDeprecations": "6.0"aocompilerOptions. Os avisos ficam em espera e o build continua passando enquanto o time se organiza. - Limpe os avisos. Remova o
ignoreDeprecations, rodenpx tsc --noEmite resolva um aviso por vez, usando o bloco de tsconfig acima como guia. Confira também os efeitos dos novos padrões:strict,typesenoUncheckedSideEffectImports. - Troque para o 7.0. Com o 6.0 limpo, instale
typescript@7. Se alguma ferramenta reclamar da API, deixe o 6.0 ao lado com o alias. - Ajuste a CI. Na integração contínua (o servidor que roda testes a cada push), use
tsc --noEmitdo 7 como etapa de checagem de tipos. Ela fica bem mais curta. Mantenha as etapas que dependem do 6.0 apontando para o pacote com alias. - Acerte o editor. Configure o editor para usar a versão do TypeScript instalada no projeto, e não a que vem embutida nele. Em projetos Vue ou Svelte, confirme que o plugin da linguagem continua funcionando antes de liberar a mudança para o time inteiro.
Rodar TypeScript: execute com o runtime, confira com o tsc
Execução e checagem são etapas separadas
Um runtime (ambiente que executa código) pode rodar um arquivo TypeScript sem conferir se os tipos estão corretos. Já o tsc (compilador do TypeScript), com a opção --noEmit, verifica o projeto sem gerar arquivos JavaScript. Na Mirante, essa separação permite executar rapidamente o formulário de captura e manter uma checagem explícita antes da publicação.
A transpilação (transformação do código em outra forma executável) também pode acontecer dentro de uma ferramenta. Vite e Next.js fazem essa transformação para executar a aplicação; o tsc --noEmit fica responsável apenas pela conferência quando chamado dessa maneira. O Next.js também pode integrar a verificação ao processo de construção da aplicação, então confira a configuração do projeto antes de duplicar etapas.
Escolha o executor pelo código que precisa rodar
O Node.js executa arquivos .ts diretamente com type stripping (remoção das anotações de tipo antes da execução). Esse recurso está estável, e o Node 26 é a linha LTS atual (linha com suporte prolongado). O comando básico é node app.ts, sem uma etapa anterior para gerar JavaScript.
Deno separa as tarefas em deno run, para executar, e deno check, para verificar tipos. Bun executa .ts e .tsx diretamente, sem checar os tipos. O tsx é um executor apoiado no esbuild (ferramenta de transformação de código), útil quando o projeto precisa de sintaxes que o Node puro rejeita.
| Runtime ou executor | Comando de execução | Verifica tipos ao executar? | Aceita enum? | Aceita .tsx? |
|---|---|---|---|---|
| Node.js | node app.ts | Não | Não | Não |
| Deno | deno run app.ts | Não; use deno check app.ts | Sim | Sim, com configuração de JSX |
| Bun | bun run app.ts | Não | Sim | Sim |
Node.js via tsx | npx tsx app.ts | Não | Sim | Sim |
A extensão .tsx permite JSX (sintaxe de marcação usada para descrever interfaces dentro do código). Aceitar essa extensão significa conseguir transformar a sintaxe; renderizar a interface ainda depende das bibliotecas e da configuração da aplicação. Para uma captura de leads sem interface no servidor, um arquivo .ts costuma bastar.
O Node aceita sintaxe que pode ser apagada
Sintaxe apagável, ou erasable, é aquela cujas partes específicas do TypeScript podem ser removidas sem criar instruções JavaScript no lugar. Anotações de parâmetros, tipos de retorno, type, interface, as const e import type se encaixam nesse modelo. O restante do programa continua sendo JavaScript executável.
O Node rejeita construções que exigem transformação adicional: enum (declaração de um conjunto de valores nomeados), namespace com código, import x = require(...) e parameter properties (parâmetros do construtor que também declaram propriedades). Ele também não executa .tsx. Para manter esses recursos, use um executor que faça a transformação ou gere JavaScript antes de executar.
O bloco abaixo é válido para o compilador TypeScript em modo estrito. As duas primeiras declarações, porém, são rejeitadas pelo executor direto do Node. As declarações seguintes mostram versões apagáveis para representar o mesmo estado e armazenar o orçamento em centavos.
enum EstadoComEnum {
Novo = "novo",
Qualificado = "qualificado",
}
class CampanhaComAtalho {
constructor(public orcamentoCentavos: number) {}
}
const EstadoLead = {
Novo: "novo",
Qualificado: "qualificado",
} as const;
type EstadoLead = (typeof EstadoLead)[keyof typeof EstadoLead];
class Campanha {
public orcamentoCentavos: number;
constructor(orcamentoCentavos: number) {
this.orcamentoCentavos = orcamentoCentavos;
}
}
const estado: EstadoLead = EstadoLead.Novo;
const campanha = new Campanha(150_000);
console.log(estado, campanha.orcamentoCentavos);- resultado
sem erro de tipo, com strict
Na reescrita, as const preserva os valores literais (tipos que representam valores exatos, como "novo"). O tipo derivado usa typeof para obter o tipo do objeto e keyof para selecionar suas chaves, produzindo a união "novo" | "qualificado" (tipo que aceita uma dessas alternativas). Na classe, a declaração da propriedade é apagada, enquanto a atribuição dentro do construtor permanece.
Configure a conferência para combinar com o Node
O Node não lê o tsconfig.json (arquivo de configuração do compilador TypeScript). Nos imports relativos (referências a arquivos por caminhos como ./lead.ts), ele exige a extensão .ts; para importar apenas tipos, use import type. Configurações que passam no compilador ainda precisam corresponder ao que o runtime consegue executar.
Para o serviço da Mirante, instale o compilador e @types/node (declarações de tipos das funcionalidades do Node). O projeto abaixo usa módulos ESM (sistema de módulos baseado em import e export). O campo "type": "module" no próximo arquivo define essa escolha para o pacote.
npm init -y
npm install --save-dev typescript @types/node
# Após salvar os arquivos abaixo:
npm run check
npm test
npm run devOs scripts (atalhos de comandos do pacote) deixam explícita a função de cada etapa. Salve o conteúdo abaixo como package.json, incorporando também as dependências registradas pela instalação. O comando test executa um arquivo específico com o executor de testes do próprio Node, sem conferir tipos.
{
"name": "mirante-captura",
"private": true,
"type": "module",
"scripts": {
"dev": "node app.ts",
"check": "tsc --noEmit",
"test": "node --test lead.test.ts"
}
}No tsconfig.json, erasableSyntaxOnly faz o compilador apontar construções incompatíveis com a estratégia de apagar tipos. allowImportingTsExtensions permite conferir os caminhos terminados em .ts neste projeto sem emissão de arquivos. A lista types inclui explicitamente os tipos do Node, pois os pacotes @types não entram automaticamente nas configurações atuais.
{
"compilerOptions": {
"target": "es2025",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"types": ["node"]
},
"include": ["*.ts"]
}As declarações com enum e com o atalho no construtor do exemplo anterior serão recusadas se forem copiadas para este projeto. Mantenha apenas as versões apagáveis nos arquivos executados com node. A opção do compilador antecipa essa incompatibilidade durante a checagem.
Capture um lead com HTTP do próprio Node
O servidor recebe uma requisição HTTP (protocolo de comunicação entre cliente e servidor) com corpo em JSON (formato textual de dados). O tipo Lead descreve o formato esperado pelo código, mas os dados recebidos precisam de validação em execução. A função abaixo recebe unknown (tipo que exige verificações antes do uso) e aceita somente objetos com nome e e-mail preenchidos.
export type Lead = {
nome: string;
email: string;
};
export function validarLead(valor: unknown): Lead | undefined {
if (typeof valor !== "object" || valor === null) return undefined;
if (!("nome" in valor) || !("email" in valor)) return undefined;
if (typeof valor.nome !== "string") return undefined;
if (typeof valor.email !== "string") return undefined;
const nome = valor.nome.trim();
const email = valor.email.trim();
if (nome.length === 0 || email.length === 0) return undefined;
return { nome, email };
}- resultado
sem erro de tipo, com strict
Essa validação confere presença e conteúdo textual; a verificação do formato do e-mail pode ser acrescentada conforme a regra do formulário. O servidor importa a função como código e Lead com import type, que será apagado. O módulo node:http fornece o servidor sem instalar um framework (biblioteca que organiza a aplicação).
import { createServer } from "node:http";
import type { IncomingMessage, ServerResponse } from "node:http";
import { validarLead } from "./lead.ts";
import type { Lead } from "./lead.ts";
async function capturarLead(
requisicao: IncomingMessage,
resposta: ServerResponse,
): Promise<void> {
resposta.setHeader("Content-Type", "application/json; charset=utf-8");
if (requisicao.method !== "POST" || requisicao.url !== "/leads") {
resposta.writeHead(404).end(JSON.stringify({ erro: "Rota ausente" }));
return;
}
try {
requisicao.setEncoding("utf8");
let corpo = "";
for await (const parte of requisicao) corpo += parte;
const dados: unknown = JSON.parse(corpo);
const lead: Lead | undefined = validarLead(dados);
if (!lead) {
resposta.writeHead(400).end(JSON.stringify({ erro: "Lead inválido" }));
return;
}
resposta.writeHead(201).end(JSON.stringify({ recebido: true }));
} catch {
resposta.writeHead(400).end(JSON.stringify({ erro: "Corpo inválido" }));
}
}
createServer(capturarLead).listen(3000, "127.0.0.1", () => {
console.log("Mirante: captura em http://127.0.0.1:3000/leads");
});O endereço 127.0.0.1 mantém o exemplo acessível apenas na máquina local. A resposta de sucesso confirma o recebimento, sem salvar o lead. Antes de publicar, acrescente limite de tamanho do corpo, tratamento de erros de infraestrutura, controles de acesso e persistência (armazenamento dos dados).
Execute testes e tipos em comandos próprios
O teste verifica um comportamento durante a execução: aceitar um lead preenchido e rejeitar outro incompleto. A checagem estática (análise do código sem executá-lo) continua sendo responsabilidade de npm run check. Como o arquivo de teste está incluído no projeto, seus tipos também entram nessa conferência.
import { test as testar } from "node:test";
import { deepStrictEqual, strictEqual } from "node:assert";
import { validarLead } from "./lead.ts";
testar("aceita um lead preenchido", () => {
const dados = { nome: "Ana", email: "ana@mirante.example" };
deepStrictEqual(validarLead(dados), dados);
});
testar("rejeita um lead sem e-mail", () => {
strictEqual(validarLead({ nome: "Ana" }), undefined);
});Em projetos com Deno, use deno check app.ts além de deno run app.ts; o servidor acima usa módulos do Node, então confirme a compatibilidade ao trocar de runtime. Com Bun ou tsx, mantenha tsc --noEmit como comando separado de conferência. Em aplicações Vite e Next.js, execute a ferramenta da aplicação no script dev e preserve uma etapa explícita de checagem no processo de publicação.
Tipos básicos: descreva o lead e deixe o compilador completar
Comece pelos valores do lead
Um tipo descreve quais valores uma variável pode receber e quais operações fazem sentido sobre ela. A inferência é a dedução desse tipo pelo compilador, a ferramenta que verifica o código TypeScript. Na Mirante, os campos de um lead já oferecem informação suficiente para muitas dessas deduções.
const lead = {
nome: "Marina",
telefone: "+55 11 99999-0000",
aceitouTermos: true,
orcamentoEmCentavos: 150000,
};
const nome = lead.nome;
// ^?
const aceitouTermos = lead.aceitouTermos;
// ^?
const orcamentoEmCentavos = lead.orcamentoEmCentavos;
// ^?- linha 7
const nome: string - linha 9
const aceitouTermos: boolean - linha 11
const orcamentoEmCentavos: number - resultado
sem erro de tipo, com strict
Os tipos inferidos nas três últimas variáveis são string, boolean e number, respectivamente. Não há necessidade de repetir essas anotações quando o valor inicial já comunica a intenção. O telefone fica como texto para preservar sinais, espaços e possíveis zeros iniciais.
| Tipo | O que representa | Uso possível |
|---|---|---|
string | Texto | Nome e telefone do lead |
number | Números inteiros ou fracionários | Orçamento em centavos e custo médio |
boolean | true ou false | Aceite dos termos |
bigint | Inteiros de precisão arbitrária | Identificador inteiro muito grande |
symbol | Identidade única | Chave interna de metadados |
null | Ausência explícita | Telefone removido pelo usuário |
undefined | Valor ausente ou ainda não definido | Campo opcional que não veio no formulário |
Guardar dinheiro em centavos ajuda a trabalhar com valores inteiros, mas number também aceita frações, NaN e valores infinitos. Uma anotação number não garante que o orçamento seja inteiro, positivo ou seguro para cálculos. Essas regras precisam de validação durante a execução.
bigint serve para inteiros que podem ultrapassar a precisão segura de number. O sufixo n identifica um literal desse tipo, e operações aritméticas exigem operandos compatíveis: misturar bigint e number diretamente produz erro. Já symbol cria uma identidade única, mesmo quando duas chamadas usam a mesma descrição.
const identificadorExterno = 9007199254740993n;
// ^?
const proximoIdentificador = identificadorExterno + 1n;
const chaveInterna = Symbol("mirante.lead");
// ^?
const lead = {
nome: "Marina",
[chaveInterna]: identificadorExterno,
};- linha 1
const identificadorExterno: 9007199254740993n - linha 4
const chaveInterna: typeof chaveInterna - resultado
sem erro de tipo, com strict
Literais e alargamento: o efeito de let e const
Um tipo literal aceita um valor específico, como "rascunho" ou true. O alargamento é a passagem desse tipo específico para um tipo mais amplo, como string ou boolean. Com let, o compilador geralmente considera futuras atribuições; com const, uma variável primitiva pode conservar seu tipo literal.
let statusEditavel = "rascunho";
statusEditavel = "publicada";
const statusFixo = "rascunho";
// ^?
const campanha = { status: "rascunho" };
// ^?
campanha.status = "publicada";
const campanhaFixa = { status: "rascunho" } as const;
// ^?- linha 3
const statusFixo: "rascunho" - linha 5
const campanha: { status: string; } - linha 8
const campanhaFixa: { readonly status: "rascunho"; } - resultado
sem erro de tipo, com strict
Nesse exemplo, statusEditavel recebe o tipo string, enquanto statusFixo recebe "rascunho". O objeto campanha tem a propriedade status: string, pois const impede reatribuir a variável, mas permite alterar suas propriedades. A asserção as const, uma instrução para preservar tipos literais e marcar propriedades como somente leitura, mantém "rascunho" em campanhaFixa.
Para limitar estados editáveis, use uma união, isto é, um tipo que aceita uma entre várias alternativas. A anotação let status: "rascunho" | "publicada" = "rascunho" permite os dois estados e rejeita outros textos. Assim, a variável continua editável dentro do conjunto previsto.
Ausência explícita com strictNullChecks
strict reúne verificações mais exigentes do compilador, incluindo strictNullChecks, que mantém null e undefined separados dos demais tipos. Com essa opção ativa, uma variável string exige texto. Para aceitar ausência, declare essa possibilidade na união.
type Lead = {
nome: string;
telefone: string | null;
aceitouTermos: boolean;
orcamentoEmCentavos?: number;
};
const lead: Lead = {
nome: "Marina",
telefone: null,
aceitouTermos: true,
};
const orcamento = lead.orcamentoEmCentavos;
// ^?
const telefoneExibido = lead.telefone ?? "Sem telefone";
// erro esperado: TS2322
const telefoneObrigatorio: string = lead.telefone;- linha 13
const orcamento: number | undefined - linha 17
TS2322: Type 'string | null' is not assignable to type 'string'. Type 'null' is not assignable to type 'string'.
O comando type cria um nome para uma descrição de tipo. O ponto de interrogação em orcamentoEmCentavos? marca uma propriedade opcional; ao lê-la, o resultado pode ser number ou undefined. O operador ??, chamado de coalescência nula, fornece uma alternativa apenas para null ou undefined, preservando valores como zero.
O erro esperado é TS2322: o campo telefone admite null, enquanto telefoneObrigatorio exige string. Para usá-lo como texto, primeiro verifique lead.telefone !== null ou forneça uma alternativa adequada. Essa verificação faz um estreitamento de tipo, também chamado de narrowing: dentro do caminho verificado, o compilador passa a conhecer um tipo mais específico.
any e unknown: escolha o tratamento dos dados externos
any permite acessar propriedades, chamar valores e atribuí-los a outros tipos sem as verificações habituais. Pode servir como saída temporária em uma migração de código antigo ou em uma integração ainda sem tipos. Seu custo é propagar a falta de verificação para expressões que dependem dele.
unknown também aceita qualquer valor, mas exige verificações antes de operações específicas. É uma escolha adequada para fronteiras externas, os pontos em que dados entram no sistema por formulários, arquivos ou webhooks (requisições enviadas por outro serviço). Anotar um dado como unknown cria a obrigação de conferir seu conteúdo antes do uso.
const legado: any = { nome: "Marina" };
const nomeLegado = legado.nome;
// ^?
export function lerNome(dado: unknown): string {
if (
typeof dado === "object" &&
dado !== null &&
"nome" in dado &&
typeof dado.nome === "string"
) {
return dado.nome;
}
throw new Error("Lead sem nome válido");
}
const recebido: unknown = JSON.parse('{"nome":"Marina"}');
const nomeValidado = lerNome(recebido);
// ^?- linha 2
const nomeLegado: any - linha 18
const nomeValidado: string - resultado
sem erro de tipo, com strict
nomeLegado tem tipo any; nomeValidado tem tipo string. JSON.parse, a função que transforma texto JSON em um valor JavaScript, não confirma a estrutura esperada do lead, por isso a entrada recebe a anotação unknown. A função verifica apenas o nome; telefone, aceite e orçamento precisariam de suas próprias regras.
Evite substituir essas verificações por uma asserção como recebido as Lead. Uma asserção de tipo informa ao compilador qual tipo considerar, sem validar nem transformar o valor durante a execução. Dados externos podem contrariar essa informação.
void e never: dois contratos de retorno
Um contrato de retorno descreve o resultado que quem chama uma função pode usar. void indica que esse contrato não oferece um resultado útil, como em uma função que apenas registra uma ação. never indica que a função não termina normalmente, por exemplo, porque sempre lança um erro.
function registrarAceite(nome: string): void {
console.log(`Aceite registrado para ${nome}`);
}
function rejeitarLead(motivo: string): never {
throw new Error(motivo);
}
export function confirmarAceite(aceitouTermos: boolean): void {
if (!aceitouTermos) {
rejeitarLead("O aceite dos termos é obrigatório");
}
registrarAceite("Marina");
}- resultado
sem erro de tipo, com strict
Use void quando o chamador deve tratar a função como uma ação, sem depender de um valor retornado. Uma função que termina sem return com valor produz undefined durante a execução. Reserve never para caminhos sem conclusão normal ou para representar possibilidades que já foram eliminadas pelas verificações.
Arrays e tuplas: escolha entre coleção e posição
Um array é uma coleção de elementos, normalmente do mesmo tipo. A escrita string[] representa uma lista de textos; readonly string[] oferece uma visão somente leitura dessa lista. Anotar uma lista vazia comunica quais elementos ela deve receber ao longo do código.
Uma tupla descreve uma sequência com posições e tipos definidos. Os nomes em uma tupla nomeada documentam o significado de cada posição e ajudam nas sugestões do editor. Eles não criam propriedades acessíveis por esses nomes.
const telefones: string[] = [];
telefones.push("+55 11 99999-0000");
const canais: readonly string[] = ["formulario", "indicacao"];
type FaixaOrcamento = readonly [
minimoEmCentavos: number,
maximoEmCentavos: number,
];
const faixa: FaixaOrcamento = [50000, 200000];
const [minimo, maximo] = faixa;
const origem = ["formulario", "mirante"] as const;
// ^?- linha 12
const origem: readonly ["formulario", "mirante"] - resultado
sem erro de tipo, com strict
Use um array para os telefones de vários leads e uma tupla para um par cuja ordem tenha significado. Na tupla faixa, os valores são acessados por índice ou por desestruturação, a extração das posições para variáveis como minimo e maximo. A tupla origem conserva os dois textos literais por causa de as const.
readonly impede alterações por aquela referência durante a verificação de tipos. Ele não congela os dados durante a execução nem torna automaticamente imutáveis os objetos dentro de uma coleção. Para um campo obrigatório como aceitouTermos, prefira boolean; uma lista de opções só precisa de leitura quando o código consumidor não deve modificá-la.
Anote as fronteiras e deixe os detalhes locais serem inferidos
Parâmetros são os valores recebidos por uma função; retornos são os valores que ela entrega. Anote parâmetros de funções independentes e retornos de funções exportadas, aquelas disponibilizadas para outros arquivos. Isso torna o contrato visível e ajuda a detectar mudanças acidentais.
Dentro da função, deixe o compilador inferir resultados evidentes. Em funções passadas como argumento, os parâmetros também podem vir da tipagem contextual, a informação fornecida pelo lugar onde a função é usada. O método map, que transforma cada item de um array, é um exemplo.
type Lead = {
nome: string;
telefone: string | null;
aceitouTermos: boolean;
orcamentoEmCentavos: number;
};
export function calcularSaldo(
lead: Lead,
gastoEmCentavos: number,
): number {
const saldo = lead.orcamentoEmCentavos - gastoEmCentavos;
// ^?
return saldo;
}
const nomes = ["Marina", "João"];
const nomesNormalizados = nomes.map((nome) => nome.trim());
// ^?- linha 12
const saldo: number - linha 18
const nomesNormalizados: string[] - resultado
sem erro de tipo, com strict
saldo é inferido como number, e nome recebe string pelo contexto de map. Já o retorno explícito de calcularSaldo documenta o resultado disponível aos outros arquivos. O tipo Lead organiza o uso interno, enquanto a entrada externa ainda precisa ser validada antes de receber esse tratamento.
- Anote: parâmetros sem contexto, retornos exportados, listas vazias com finalidade definida e entradas externas como
unknown. - Deixe inferir: cálculos locais, valores iniciais evidentes e parâmetros cujo contexto já fornece o tipo.
- Declare a intenção: use uniões para estados permitidos e ausência, além de
readonlypara referências que devem oferecer apenas leitura.
Objetos: escolha o contrato de cada dado
type e interface: uma regra para começar
Um tipo de objeto descreve as propriedades esperadas de um valor. No TypeScript, type cria um alias de tipo (um nome para representar outro tipo), enquanto interface declara um contrato de propriedades. Ambos servem para modelar leads, formulários e campanhas.
Como regra de equipe, use type como padrão e reserve interface para contratos que precisam de extensão por outras declarações. Essa escolha mantém explícita a possibilidade de reabrir um contrato. A Mirante pode adotar outra convenção, desde que as diferenças abaixo orientem a decisão.
Tabela: Diferenças práticas entre type e interface
| Necessidade | type | interface |
|---|---|---|
| Descrever propriedades de um objeto | Sim | Sim |
| Reabrir uma declaração com o mesmo nome | Não | Sim, por declaration merging |
| Herdar propriedades | Usa interseção com & | Usa extends |
Representar uma união com | | Sim | Não diretamente |
| Combinar propriedades incompatíveis | Pode produzir uma propriedade never | A extensão incompatível gera erro na declaração |
Declaration merging (fusão de declarações) reúne membros de interfaces com o mesmo nome no mesmo escopo. Já extends (extensão de um contrato) permite criar uma interface baseada em outra. Uma união (tipo que aceita uma entre várias alternativas) pode receber um nome com type.
interface Lead {
nome: string;
}
interface Lead {
email: string;
}
interface LeadQualificado extends Lead {
pontuacao: number;
}
type StatusLead = "novo" | "qualificado";
const lead: LeadQualificado = {
nome: "Ana",
email: "ana@mirante.example",
pontuacao: 80,
};
const status: StatusLead = "novo";
// ^?- linha 21
const status: StatusLead - resultado
sem erro de tipo, com strict
A fusão exige compatibilidade: duas declarações da mesma propriedade precisam concordar no tipo. Ela é útil quando um contrato foi planejado para receber contribuições de outros arquivos ou bibliotecas. Para modelos internos da agência, o type costuma deixar a composição mais localizada.
Interseções podem produzir never
Uma interseção (A & B) exige que o valor satisfaça os dois tipos ao mesmo tempo. Ao combinar objetos, isso reúne requisitos sobre suas propriedades. Quando uma mesma propriedade precisa ser simultaneamente number e string, seu tipo vira never (tipo sem nenhum valor possível).
type OrcamentoNumerico = {
orcamentoCentavos: number;
};
type OrcamentoTextual = {
orcamentoCentavos: string;
};
type CampanhaConflitante = OrcamentoNumerico & OrcamentoTextual;
const lerOrcamento = (campanha: CampanhaConflitante) =>
campanha.orcamentoCentavos;
// ^?
const campanha: CampanhaConflitante = {
// erro esperado: TS2322
orcamentoCentavos: 150000,
};- linha 12
(parameter) campanha: CampanhaConflitante - linha 17
TS2322: Type 'number' is not assignable to type 'never'.
Aqui, a propriedade orcamentoCentavos resulta em never; o objeto inteiro não precisa ser reduzido a never. Com interface e extends, a incompatibilidade dessa propriedade seria apontada na própria declaração da interface derivada. Ao combinar contratos de integrações, confira os campos repetidos antes de usar &.
Propriedades opcionais: ausência tem significado
O marcador ? torna uma propriedade opcional, permitindo que a chave esteja ausente. Na leitura, o resultado inclui undefined (valor que também representa uma propriedade ausente). Isso pede uma verificação ou um valor alternativo antes de usar métodos de string, por exemplo.
type Lead = {
nome: string;
telefone?: string;
};
const lead: Lead = { nome: "Ana" };
const telefone = lead.telefone;
// ^?
const telefoneExibido = telefone ?? "Sem telefone";
console.log(telefoneExibido);- linha 7
const telefone: string | undefined - resultado
sem erro de tipo, com strict
A opção exactOptionalPropertyTypes diferencia uma chave ausente de uma chave presente com valor undefined. Ela precisa ser habilitada separadamente de strict (conjunto de verificações rigorosas). Em um projeto, registre essa escolha no tsconfig.json (arquivo de configuração do compilador).
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noEmit": true
}
}Com essa opção ligada, telefone?: string aceita a ausência da chave ou uma string, mas rejeita a atribuição explícita de undefined. Para aceitar também esse valor, declare telefone?: string | undefined. Se a chave precisar existir sempre, use telefone: string | undefined, sem o marcador opcional.
O laboratório usa strict; o exemplo de leitura acima funciona com ou sem exactOptionalPropertyTypes. A configuração mostrada precisa ser aplicada no projeto para verificar a distinção na escrita. Essa diferença importa em integrações que tratam “campo ausente” e “campo enviado sem valor” de formas distintas.
readonly protege atribuições, com alcance limitado
O modificador readonly impede reatribuir uma propriedade pela referência tipada. O utilitário Readonly<T> aplica essa restrição a todas as propriedades do primeiro nível de T. A proteção é superficial: objetos internos continuam mutáveis, a menos que seus próprios tipos também restrinjam alterações.
type Painel = {
readonly id: string;
titulo: string;
filtros: {
campanha: string;
};
};
const painel: Readonly<Painel> = {
id: "painel-mirante",
titulo: "Captação",
filtros: { campanha: "primavera" },
};
const filtros = painel.filtros;
// ^?
painel.filtros.campanha = "verao";
// erro esperado: TS2540
painel.titulo = "Outro título";- linha 15
const filtros: { campanha: string; } - linha 21
TS2540: Cannot assign to 'titulo' because it is a read-only property.
Nesse exemplo, id já era somente leitura no tipo original, e Readonly<Painel> acrescenta a restrição a titulo e filtros. A referência painel.filtros fica protegida contra substituição, mas sua propriedade campanha aceita alteração. Essas restrições atuam na checagem de tipos e não congelam o objeto durante a execução.
Assinatura de índice e Record: escolha as chaves
Uma assinatura de índice (declaração do tipo dos valores acessados por chaves variáveis) serve para dicionários com nomes desconhecidos antecipadamente. Já Record<Chaves, Valor> é um utilitário que associa cada chave indicada a um tipo de valor. Quando as chaves formam uma união finita, Record exige todas elas.
type MetricasLivres = {
[nome: string]: number | undefined;
};
type Canal = "email" | "anuncio";
const metricas: MetricasLivres = {
visualizacoes: 3200,
};
const cliques = metricas["cliques"];
// ^?
const leadsPorCanal: Record<Canal, number> = {
email: 42,
anuncio: 78,
};
const leadsEmail = leadsPorCanal.email;
// ^?- linha 11
const cliques: number | undefined - linha 19
const leadsEmail: number - resultado
sem erro de tipo, com strict
A assinatura acima inclui undefined porque uma chave consultada pode estar ausente. Record<Canal, number> exige email e anuncio, enquanto Record<string, number> descreve um conjunto aberto de chaves e não garante que toda consulta encontre um valor. Para dicionários abertos, explicite essa possibilidade no valor ou habilite noUncheckedIndexedAccess (opção que acrescenta undefined aos acessos sem presença garantida).
Propriedades nomeadas também precisam respeitar a assinatura de índice. Se ela aceita apenas números, uma propriedade como titulo: string entra em conflito com esse contrato. Separar metadados e métricas em objetos internos costuma deixar essa estrutura mais clara.
Literais recebem uma verificação de propriedades extras
Um literal de objeto é um valor escrito diretamente com { ... }. Ao atribuí-lo a um tipo conhecido, o TypeScript aplica a verificação de propriedades em excesso, chamada excess property check. Ela ajuda a encontrar campos digitados incorretamente ou enviados para o contrato errado.
type Lead = {
nome: string;
email: string;
};
const leadDireto: Lead = {
nome: "Ana",
email: "ana@mirante.example",
// erro esperado: TS2353
origem: "formulario",
};
const entrada = {
nome: "Bia",
email: "bia@mirante.example",
origem: "webhook",
};
const leadImportado: Lead = entrada;
// ^?- linha 19
const leadImportado: Lead - linha 10
TS2353: Object literal may only specify known properties, and 'origem' does not exist in type 'Lead'.
A atribuição de entrada é aceita pela compatibilidade estrutural (comparação baseada nas propriedades exigidas pelo tipo). O valor tem nome e email compatíveis, mesmo carregando uma propriedade adicional. Essa atribuição preserva origem durante a execução, embora o acesso por leadImportado exponha apenas o contrato Lead.
Essa verificação também não valida dados externos em tempo de execução. Para um webhook (requisição enviada automaticamente por uma integração), valide a entrada antes de tratá-la como Lead. Criar uma variável intermediária apenas para contornar o aviso reduz a chance de detectar um erro de nome.
Objetos aninhados organizam a campanha
Um objeto aninhado é uma propriedade cujo valor também é um objeto. Agrupar orçamento, responsável e métricas mantém próximos os campos que compartilham significado. Extraia tipos com nome quando o grupo for reutilizado ou merecer um contrato próprio.
type Responsavel = {
nome: string;
email: string;
};
type Campanha = {
readonly id: string;
titulo: string;
responsavel: Responsavel;
orcamento: {
limiteCentavos: number;
gastoCentavos: number;
};
metricas: Record<"leads" | "formulariosEnviados", number>;
integracao: {
webhookUrl?: string;
};
};
const campanha: Campanha = {
id: "campanha-mirante-01",
titulo: "Captação de clientes",
responsavel: { nome: "Ana", email: "ana@mirante.example" },
orcamento: {
limiteCentavos: 250000,
gastoCentavos: 82500,
},
metricas: { leads: 120, formulariosEnviados: 96 },
integracao: {},
};
const limite = campanha.orcamento.limiteCentavos;
// ^?- linha 32
const limite: number - resultado
sem erro de tipo, com strict
Os nomes limiteCentavos e gastoCentavos deixam explícita a unidade usada pela Mirante. O tipo number, sozinho, também aceita valores fracionários ou negativos, então essas regras dependem de validação em tempo de execução (checagem feita enquanto o programa roda). Se a interface permitir editar a campanha, valide essas condições antes de salvar o orçamento ou enviá-lo à integração.
Uniões: trate cada valor e cada estado do formulário
Uma união (tipo que aceita alternativas) descreve valores como string | number: uma string ou um número. Para acessar uma operação específica, o TypeScript precisa identificar qual alternativa está naquele trecho do código. Esse processo é o estreitamento, ou narrowing (redução das possibilidades de um tipo a partir de verificações).
Na Mirante, isso aparece no orçamento recebido de um formulário, na captura de leads e nos estados de um botão de envio. As verificações feitas durante a execução também orientam o compilador, que acompanha quais tipos continuam possíveis em cada caminho.
typeof: separe tipos primitivos
O operador typeof (consulta do tipo de um valor durante a execução) ajuda a separar tipos primitivos, como strings e números. No exemplo, o orçamento pode chegar como texto digitado ou como um número já convertido para centavos. Dentro de cada ramo do if, o TypeScript permite as operações da alternativa correspondente.
function lerCentavos(orcamento: string | number): number {
if (typeof orcamento === "string") {
const texto = orcamento.trim();
return Number(texto);
}
return orcamento;
}
const centavos = lerCentavos("15000");
console.log(centavos);- resultado
sem erro de tipo, com strict
O exemplo considera que o texto já representa centavos. A conversão com Number ainda pode produzir NaN (valor numérico que representa um resultado inválido), então a regra de negócio precisa conferir o resultado antes de salvar o orçamento. O estreitamento autoriza operações sobre o tipo identificado, enquanto a validação decide quais valores a agência aceita.
Há também uma particularidade do JavaScript: typeof null retorna "object". Por isso, verificar apenas typeof valor === "object" ainda deixa null entre as possibilidades. Para examinar propriedades de um objeto desconhecido, faça também a comparação valor !== null.
Igualdade: elimine alternativas com comparações explícitas
A igualdade estrita, escrita como ===, compara valores sem conversão automática. Ao comparar uma variável com null, o compilador consegue eliminar essa alternativa no caminho seguinte. Uma comparação entre duas variáveis também pode revelar o tipo que ambas têm em comum.
function prepararNome(nome: string | null): string {
if (nome === null) {
return "Sem identificação";
}
return nome.trim();
}
function compararChaves(
chaveFormulario: string | number,
chaveCampanha: string | boolean,
): string {
if (chaveFormulario === chaveCampanha) {
return chaveFormulario.toUpperCase();
}
return "Chaves diferentes";
}- resultado
sem erro de tipo, com strict
Em compararChaves, a única alternativa compartilhada pelas duas uniões é string. Quando a igualdade é verdadeira, as duas variáveis ficam estreitadas para esse tipo. Comparações com valores literais (valores específicos usados como tipos), como "enviado", também serão úteis para representar estados do formulário.
Truthiness: zero e texto vazio também caem no ramo falso
Truthiness (conversão implícita de um valor para verdadeiro ou falso) aparece em condições como if (valor). Entre os valores tratados como falsos estão 0, "", null e undefined. Isso exige cuidado quando zero é um orçamento válido ou quando o texto vazio tem significado próprio.
function rotuloArriscado(centavos: number | undefined): string {
if (centavos) {
return `${centavos} centavos`;
}
return "Orçamento ausente";
}
function rotuloOrcamento(centavos: number | undefined): string {
if (centavos === undefined) {
return "Orçamento ausente";
}
return `${centavos} centavos`;
}
function prepararObservacao(observacao: string | null): string {
if (observacao === null) {
return "Observação ausente";
}
return observacao.trim();
}
console.log(rotuloArriscado(0));
console.log(rotuloOrcamento(0));
console.log(prepararObservacao(""));- resultado
sem erro de tipo, com strict
A primeira função trata zero como ausência; a segunda preserva o orçamento de zero centavos. A terceira devolve uma string vazia quando recebe "", mantendo a distinção entre campo vazio e campo ausente. Se a regra do formulário exigir texto preenchido, confira o comprimento depois de remover os espaços com trim().
Prefira comparações explícitas quando o objetivo for excluir apenas null ou undefined. Use truthiness quando agrupar todos os valores falsos fizer parte da regra. Essa escolha fica especialmente importante em filtros de listas.
in e instanceof: reconheça propriedades e instâncias
O operador in (teste de existência de uma propriedade no objeto ou em sua cadeia de protótipos) ajuda a distinguir formatos de objetos. Já instanceof (teste de vínculo com o protótipo de um construtor) reconhece instâncias, como objetos criados com Date. Nos dois casos, a verificação acontece durante a execução e orienta o estreitamento.
type Lead = { email: string };
type Campanha = { titulo: string };
function identificar(item: Lead | Campanha): string {
if ("email" in item) {
return item.email;
}
return item.titulo;
}
function formatarRecebimento(recebidoEm: Date | string): string {
if (recebidoEm instanceof Date) {
return recebidoEm.toISOString();
}
return recebidoEm.trim();
}
console.log(identificar({ email: "contato@mirante.example" }));
console.log(formatarRecebimento(new Date()));- resultado
sem erro de tipo, com strict
Aqui, email é obrigatório em Lead e está ausente de Campanha, o que permite separar as alternativas. Uma propriedade opcional (marcada com ?, portanto possivelmente ausente) pode deixar o mesmo tipo nos dois ramos de um teste com in. Além disso, encontrar uma propriedade em dados externos ainda exige conferir o tipo de seu conteúdo.
Uma data recebida em JSON (formato textual de troca de dados) costuma chegar como string. Ela só passa em instanceof Date depois de ser convertida em uma instância de Date. Essa conversão também precisa lidar com datas inválidas antes de chamar toISOString().
Predicados inferidos: filtre undefined sem repetir o tipo
Um predicado de tipo (informação de que uma condição verdadeira identifica um tipo) permite que uma função ajude o compilador a estreitar valores. Desde o TypeScript 5.5, condições como lead => lead !== undefined podem gerar esse predicado por inferência, isto é, por dedução do compilador. Com isso, filter já devolve a lista sem undefined.
type Lead = { email: string };
const recebidos: (Lead | undefined)[] = [
{ email: "contato@mirante.example" },
undefined,
];
const leads = recebidos.filter(lead => lead !== undefined);
// ^?
for (const lead of leads) {
console.log(lead.email);
}- linha 8
const leads: Lead[] - resultado
sem erro de tipo, com strict
O tipo inferido de leads é Lead[]. A condição informa exatamente qual alternativa deve ser removida, e o laço pode acessar email sem uma verificação adicional. Evite substituir esse filtro por filter(Boolean) esperando o mesmo estreitamento: essa forma também remove valores como zero e string vazia, além de não expressar o mesmo predicado para o compilador.
Predicados escritos à mão: declare o que a checagem garante
Um predicado escrito como valor is Lead declara que, quando a função retorna true, o argumento pode ser tratado como Lead. Ele é útil na entrada de um webhook (requisição enviada por outro sistema para comunicar um evento), cujo conteúdo começa como unknown (tipo que exige verificação antes do uso). O TypeScript confia nessa declaração, então a implementação precisa conferir todas as propriedades exigidas pelo tipo.
Uma assertion function (função que garante uma condição ao retornar normalmente) usa asserts para comunicar uma garantia semelhante. No exemplo abaixo, exigirLead lança um erro para interromper o caminho inválido. Depois da chamada bem-sucedida, o argumento está estreitado para Lead.
type Lead = { email: string };
function ehLead(valor: unknown): valor is Lead {
return (
typeof valor === "object" &&
valor !== null &&
"email" in valor &&
typeof valor.email === "string"
);
}
function exigirLead(valor: unknown): asserts valor is Lead {
if (!ehLead(valor)) {
throw new Error("Lead sem email em formato de texto");
}
}
function lerEmailComCondicao(entrada: unknown): string {
if (ehLead(entrada)) {
return entrada.email;
}
return "Lead inválido";
}
function lerEmailObrigatorio(entrada: unknown): string {
exigirLead(entrada);
return entrada.email;
}- resultado
sem erro de tipo, com strict
A sequência com && (operador que só avalia a próxima condição se a anterior for verdadeira) permite acessar cada informação com segurança. Primeiro vem o objeto diferente de null, depois a presença de email e, por último, o tipo da propriedade. Essa checagem aceita qualquer string como email; uma regra de formato exigiria validação adicional.
Predicados manuais e funções com asserts merecem testes de entradas válidas e inválidas. Se Lead ganhar uma propriedade obrigatória, a checagem também precisa mudar. O compilador verifica a assinatura e o corpo da função, mas confia na promessa de estreitamento declarada pelo autor.
União discriminada: cada estado leva seus próprios dados
Uma união discriminada (união de objetos identificados por uma propriedade com valores literais distintos) organiza os estados do envio. Aqui, status é o discriminante (campo usado para reconhecer cada alternativa). O estado "enviado" carrega o identificador do lead, enquanto "erro" carrega uma mensagem.
Esse formato mantém os dados ligados ao estado em que fazem sentido. O código só acessa leadId depois de reconhecer "enviado", por exemplo. Isso reduz combinações incoerentes, como um estado ocioso com uma mensagem obrigatória de falha.
type EstadoEnvio =
| { status: "ocioso" }
| { status: "enviando" }
| { status: "enviado"; leadId: string }
| { status: "erro"; mensagem: string };
function textoDoBotao(estado: EstadoEnvio): string {
switch (estado.status) {
case "ocioso":
return "Enviar cadastro";
case "enviando":
return "Enviando cadastro";
case "enviado":
return `Lead ${estado.leadId} cadastrado`;
case "erro":
return `Tentar novamente: ${estado.mensagem}`;
default: {
const impossivel: never = estado;
return impossivel;
}
}
}
const estado: EstadoEnvio = {
status: "enviado",
leadId: "lead-mirante-42",
};
console.log(textoDoBotao(estado));- resultado
sem erro de tipo, com strict
O switch (seleção de um caminho conforme o valor) estreita estado em cada case. Depois que todas as alternativas retornam, o ramo default fica com o tipo never (tipo que representa ausência de valores possíveis). A atribuição a impossivel funciona como uma verificação de exaustividade (garantia de que todas as alternativas foram tratadas).
never: faça o compilador apontar o estado esquecido
Ao remover o tratamento de "erro", sobra uma alternativa no ramo default. Nesse ponto, estado ainda pode ser { status: "erro"; mensagem: string }, que não pode ser atribuído a never. O bloco abaixo produz intencionalmente o diagnóstico TS2322 (atribuição de um tipo incompatível).
type EstadoEnvio =
| { status: "ocioso" }
| { status: "enviando" }
| { status: "enviado"; leadId: string }
| { status: "erro"; mensagem: string };
function textoDoBotao(estado: EstadoEnvio): string {
switch (estado.status) {
case "ocioso":
return "Enviar cadastro";
case "enviando":
return "Enviando cadastro";
case "enviado":
return `Lead ${estado.leadId} cadastrado`;
default: {
// erro esperado: TS2322
const impossivel: never = estado;
return impossivel;
}
}
}- linha 17
TS2322: Type '{ status: "erro"; mensagem: string; }' is not assignable to type 'never'.
Para corrigir, adicione case "erro" com seu retorno antes de default, como no bloco anterior. Se a Mirante acrescentar outro estado ao formulário, essa mesma atribuição indicará os pontos que ainda precisam tratá-lo. Mantenha a verificação com never nos trechos em que cada estado exige uma decisão explícita.
Funções: parâmetros claros, retornos previsíveis
Anote a entrada e confira a saída
Uma função reúne uma operação que pode ser chamada pelo nome, como preparar a confirmação de um formulário da Mirante. A anotação de tipo, escrita depois de :, define quais valores um parâmetro recebe ou uma função devolve. O retorno pode ser inferido (deduzido pelo compilador), mas anotá-lo ajuda a conferir contratos públicos, isto é, os acordos de entrada e saída usados por outras partes do projeto.
Um parâmetro opcional, marcado com ?, pode ser omitido e precisa considerar undefined (ausência de valor). Um parâmetro com valor padrão recebe esse valor quando o argumento é omitido ou passado como undefined. No exemplo, origem é tratada como string dentro da função porque o padrão já cobre essa ausência.
function resumirLead(
nome: string,
origem = "formulário",
campanha?: string,
): string {
const contexto = campanha ?? "sem campanha";
return `${nome} veio de ${origem}, ${contexto}`;
}
const resumo = resumirLead("Ana");
// ^?
resumirLead("Bruno", undefined, "Black Friday");
// erro esperado: TS2345
resumirLead(42);- linha 10
const resumo: string - linha 15
TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
O operador ?? usa o valor da direita quando encontra null ou undefined. Já ? no parâmetro permite deixar o argumento de fora, inclusive em uma chamada com apenas o nome. O erro esperado mostra a checagem de argumentos: o primeiro parâmetro exige texto.
Junte argumentos com rest
Um parâmetro rest (os argumentos restantes reunidos em uma lista) usa ... e fica por último. Seu tipo descreve a lista inteira: number[] significa um array, ou lista, de números. Para somar valores de campanhas, a Mirante mantém o orçamento em centavos e recebe quantos valores forem necessários.
function somarOrcamentos(...valoresCentavos: number[]): number {
return valoresCentavos.reduce(
(total, valorCentavos) => total + valorCentavos,
0,
);
}
const totalCentavos = somarOrcamentos(15000, 25000, 9000);
// ^?
const semCampanhas = somarOrcamentos();- linha 8
const totalCentavos: number - resultado
sem erro de tipo, com strict
A função também aceita uma chamada sem argumentos, cujo resultado é zero. O método reduce acumula os elementos da lista usando o valor inicial informado. Esses tipos verificam que as entradas são números; regras como aceitar apenas inteiros não negativos precisam de validação durante a execução.
Descreva callbacks e entenda o retorno void
Um tipo de função descreve seus parâmetros e seu retorno, sem precisar trazer uma implementação. Um callback é uma função entregue a outra para ser chamada durante uma operação. A assinatura (descrição dessas entradas e saída) permite que componentes, formulários e integrações combinem como essa chamada acontece.
type Lead = { nome: string };
type AoCapturar = (lead: Lead) => void;
function capturarLead(lead: Lead, aoCapturar: AoCapturar): void {
aoCapturar(lead);
}
const nomes: string[] = [];
const registrar: AoCapturar = (lead) => nomes.push(lead.nome);
capturarLead({ nome: "Ana" }, registrar);
const retornoIgnorado = registrar({ nome: "Bruno" });
// ^?- linha 13
const retornoIgnorado: void - resultado
sem erro de tipo, com strict
O retorno void indica que o chamador deve ignorar o resultado da função. Por isso, um callback desse tipo aceita uma implementação que devolve algum valor: push retorna o novo tamanho da lista, e a atribuição continua válida. Ao chamar registrar pelo tipo AoCapturar, o resultado disponível para o código é void.
Essa permissão pertence ao contrato do callback. Em uma função cuja própria implementação declara : void, devolver explicitamente um número produz erro. Para uma operação assíncrona, modele o retorno como Promise<void> quando quem chama precisar aguardar sua conclusão ou tratar sua falha.
Use sobrecargas quando a entrada muda o retorno
Sobrecargas, ou overloads, oferecem assinaturas diferentes para uma mesma função. Na busca da Mirante, um identificador devolve um lead ou ausência, enquanto um filtro devolve uma lista. As assinaturas públicas preservam essa relação para quem chama.
type Lead = {
id: string;
nome: string;
campanha: string;
};
type Filtro = { campanha: string };
const leads: Lead[] = [
{ id: "l1", nome: "Ana", campanha: "Design" },
{ id: "l2", nome: "Bruno", campanha: "Conteúdo" },
];
function buscarLead(id: string): Lead | undefined;
function buscarLead(filtro: Filtro): Lead[];
function buscarLead(
entrada: string | Filtro,
): Lead | undefined | Lead[] {
if (typeof entrada === "string") {
return leads.find((lead) => lead.id === entrada);
}
return leads.filter((lead) => lead.campanha === entrada.campanha);
}
const encontrado = buscarLead("l1");
// ^?
const filtrados = buscarLead({ campanha: "Design" });
// ^?- linha 24
const encontrado: Lead | undefined - linha 26
const filtrados: Lead[] - resultado
sem erro de tipo, com strict
A implementação aceita uma união (um tipo que permite alternativas, como string | Filtro) e trata cada possibilidade. O teste com typeof faz um estreitamento de tipo: depois da condição, o compilador conhece melhor o valor disponível em cada caminho. A assinatura da implementação precisa ser compatível com as sobrecargas e fica fora das opções públicas de chamada.
Há um cuidado ao receber uma variável já tipada como string | Filtro: nenhuma das duas sobrecargas públicas aceita essa união inteira. Nesse caso, separe os caminhos antes da chamada ou exponha uma assinatura que aceite a união e devolva os retornos possíveis. Essa assinatura adicional oferece flexibilidade, mas seu resultado também exige tratamento das alternativas.
Prefira uma união quando o contrato for o mesmo
Quando entradas diferentes recebem o mesmo tratamento e produzem o mesmo tipo de saída, uma união costuma bastar. Converter um identificador numérico ou textual para texto é um exemplo. Criar duas sobrecargas para essa operação acrescentaria assinaturas sem melhorar o tipo do resultado.
function normalizarId(valor: string | number): string {
return String(valor).trim();
}
const id = normalizarId(123);
// ^?
normalizarId(" lead-456 ");- linha 5
const id: string - resultado
sem erro de tipo, com strict
Declare o this esperado
O this é o contexto de execução recebido pela função quando ela é chamada. TypeScript permite descrevê-lo com um parâmetro especial chamado this, que desaparece do código executado e não ocupa uma posição nos argumentos da chamada. Isso ajuda em funções que serão usadas como métodos, isto é, funções acessadas por um objeto.
type Painel = {
orcamentoCentavos: number;
};
function adicionarOrcamento(
this: Painel,
valorCentavos: number,
): void {
this.orcamentoCentavos += valorCentavos;
}
const painel = {
orcamentoCentavos: 10000,
adicionarOrcamento,
};
painel.adicionarOrcamento(2500);
adicionarOrcamento.call(painel, 1500);- resultado
sem erro de tipo, com strict
Na primeira chamada, painel fornece o contexto por estar antes do ponto. Na segunda, call informa explicitamente qual objeto será o this. Evite passar esse método solto como callback quando ele depende do objeto: preserve o contexto com uma função intermediária ou com bind (criação de uma função com contexto definido).
Uma função seta, escrita com =>, captura o this do contexto em que foi criada. Ela não declara esse parâmetro especial de this. Para receber um contexto conforme a chamada, use a forma function, como no exemplo.
Preserve relações com funções genéricas
Uma função genérica usa um parâmetro de tipo, como T, para relacionar entradas e saídas. Ao selecionar o primeiro item de uma lista, interessa preservar o tipo dos itens, seja uma lista de leads ou de valores em centavos. O compilador normalmente deduz T a partir do argumento recebido.
function primeiro<T>(itens: readonly T[]): T | undefined {
return itens[0];
}
const lead = primeiro([{ id: "l1", nome: "Ana" }]);
// ^?
const orcamento = primeiro([10000, 25000]);
// ^?
const listaVazia = primeiro<string>([]);- linha 5
const lead: { id: string; nome: string; } | undefined - linha 7
const orcamento: number | undefined - resultado
sem erro de tipo, com strict
O modificador readonly permite receber a lista sem autorizar alterações nela por esse parâmetro. O retorno inclui undefined porque a lista pode estar vazia. Em primeiro<string>([]), o argumento de tipo explícito informa qual seria o tipo dos itens mesmo sem haver elementos para orientar a inferência.
Use um genérico quando existir uma relação útil a preservar. Se a função sempre retorna texto, uma anotação direta como string costuma comunicar melhor o contrato. Se ela recebe e devolve algo ligado ao tipo de entrada, o parâmetro de tipo pode evitar conversões e verificações desnecessárias.
Retorne Promise e trate erros como unknown
Uma função async entrega uma Promise<T>: um objeto que representa um resultado futuro cujo valor, se houver sucesso, terá tipo T. Dentro dela, await espera o resultado de outra promessa e permite tratar uma rejeição, ou falha, com try/catch. O tipo Promise<Lead> descreve o valor de sucesso; ele não especifica o tipo do erro lançado.
Com useUnknownInCatchVariables habilitado, a variável de catch tem tipo unknown (valor cujo tipo ainda precisa ser verificado). Essa opção faz parte de strict, o conjunto de verificações rigorosas do compilador, salvo configuração explícita em contrário. Como JavaScript permite lançar valores de qualquer tipo, confirme o formato antes de acessar propriedades como message.
type Lead = { id: string; nome: string };
async function buscarLeadRemoto(id: string): Promise<Lead> {
if (id.trim() === "") {
throw new Error("Informe o identificador do lead");
}
return { id, nome: "Ana" };
}
async function carregarLead(
buscar: () => Promise<Lead>,
): Promise<Lead | undefined> {
try {
const lead = await buscar();
return lead;
} catch (erro) {
if (erro instanceof Error) {
console.error(erro.message);
} else {
console.error("Falha com valor inesperado", erro);
}
return undefined;
}
}
const carregamento = carregarLead(() => buscarLeadRemoto("l1"));
// ^?- linha 26
const carregamento: Promise<Lead | undefined> - resultado
sem erro de tipo, com strict
O teste instanceof Error verifica se o valor é uma instância de Error e libera o acesso tipado a message. Aqui, a decisão da aplicação é registrar a falha e devolver undefined, possibilidade declarada no retorno. Se o chamador precisar reagir à falha, deixe a rejeição seguir ou lance o erro novamente com throw erro.
O await dentro do try faz diferença: ele permite que esse catch receba a rejeição de buscar(). Devolver diretamente buscar() sem aguardá-la nesse bloco deixaria uma rejeição posterior seguir para o chamador. Em integrações e webhooks, escolha explicitamente qual camada deve registrar, recuperar ou propagar cada falha.
Genéricos: preserve o tipo entre a entrada e a saída
Genéricos são parâmetros de tipo: espaços preenchidos pelo TypeScript conforme o código usa uma função, interface ou outro tipo. Eles permitem expressar relações como “o tipo do item que entra determina o tipo do item que sai”. Na agência Mirante, essa relação aparece em listas de leads, seletores de canais e respostas de integrações.
Comece pela relação entre entrada e saída
Uma função genérica declara seus parâmetros de tipo entre < e >. Em primeiro<T>, o nome T representa o tipo dos elementos da lista. A inferência de argumentos genéricos é o processo pelo qual o compilador descobre esse tipo a partir dos valores recebidos.
function primeiro<T>(itens: readonly T[]): T | undefined {
return itens[0];
}
const canal = primeiro(["email", "whatsapp"]);
// ^?
type Lead = {
id: string;
email: string;
};
const lead = primeiro([
{ id: "lead-01", email: "contato@mirante.example" },
]);
// ^?
const filaVazia = primeiro<Lead>([]);
// ^?- linha 5
const canal: string | undefined - linha 13
const lead: { id: string; email: string; } | undefined - linha 18
const filaVazia: Lead | undefined - resultado
sem erro de tipo, com strict
O modificador readonly impede alterações na lista por essa referência. O retorno T | undefined é uma união, um tipo que aceita alternativas: um elemento ou a ausência dele. Assim, a assinatura também descreve o caso da lista vazia.
Na chamada com canais, o compilador infere T como string. Na chamada com leads, ele infere a estrutura do objeto, incluindo id e email. Em primeiro<Lead>([]), o argumento de tipo explícito fornece a informação que a lista vazia não traz.
Prefira a inferência quando os argumentos já descrevem o tipo desejado. Passe o tipo explicitamente quando faltar informação, quando quiser uma união específica ou quando precisar conferir a entrada contra um contrato declarado. Essa escolha continua sujeita à verificação do compilador.
Use extends para exigir uma estrutura mínima
Uma restrição com extends define quais tipos podem ocupar o parâmetro genérico. Em T extends { id: string }, qualquer valor aceito precisa ter um id textual. Outras propriedades continuam disponíveis no retorno.
function conferirId<T extends { id: string }>(registro: T): T {
if (registro.id.length === 0) {
throw new Error("O identificador precisa estar preenchido");
}
return registro;
}
const campanha = conferirId({
id: "campanha-outono",
nome: "Captação Mirante",
orcamentoCentavos: 250_000,
});
// ^?
const orcamento = campanha.orcamentoCentavos;
// ^?- linha 8
const campanha: { id: string; nome: string; orcamentoCentavos: number; } - linha 15
const orcamento: number - resultado
sem erro de tipo, com strict
O compilador permite acessar registro.id porque a restrição garante essa propriedade. Como o retorno é T, a campanha mantém nome e orcamentoCentavos no tipo. Retornar apenas { id: string } reduziria a informação disponível para quem chama a função.
A verificação de tamanho acontece em tempo de execução, quando o programa roda. Já extends participa da checagem estática, feita sobre o código. Dados recebidos de formulários e integrações ainda precisam de validação antes de serem tratados como valores confiáveis.
Relacione a chave escolhida ao valor com keyof e T[K]
O operador keyof produz uma união das chaves de um tipo. Para um lead com id, email e ativo, ele representa "id" | "email" | "ativo". O acesso indexado T[K] consulta o tipo da propriedade K dentro de T.
function pegar<T, K extends keyof T>(objeto: T, chave: K): T[K] {
return objeto[chave];
}
const lead = {
id: "lead-02",
email: "ana@mirante.example",
ativo: true,
};
const email = pegar(lead, "email");
// ^?
const ativo = pegar(lead, "ativo");
// ^?
const identificador = pegar(lead, "id");
// ^?- linha 11
const email: string - linha 14
const ativo: boolean - linha 17
const identificador: string - resultado
sem erro de tipo, com strict
Aqui, K extends keyof T restringe a chave às propriedades existentes no objeto. O retorno acompanha a chave escolhida: email resulta em string, enquanto ativo resulta em boolean. Uma chave como "telefone" seria rejeitada porque esse objeto não declara tal propriedade.
A inferência trabalha com os dois argumentos em conjunto. O objeto fornece T, e a chave fornece K. Escrever pegar<typeof lead, "email">(...) seria possível, mas acrescentaria informação que a chamada já contém.
Preserve os literais de uma lista com const
Um tipo literal representa um valor específico, como "email", em vez de qualquer texto. Parâmetros de tipo const favorecem essa inferência mais específica em argumentos escritos diretamente na chamada. Isso ajuda a transformar uma lista de canais em uma fonte de tipos para os componentes da Mirante.
function definirCanais<const T extends readonly string[]>(canais: T): T {
return canais;
}
const canais = definirCanais(["email", "whatsapp", "sms"]);
// ^?
type Canal = (typeof canais)[number];
const canalInicial: Canal = "email";
// ^?- linha 5
const canais: readonly ["email", "whatsapp", "sms"] - linha 10
const canalInicial: "email" | "whatsapp" | "sms" - resultado
sem erro de tipo, com strict
O tipo de canais preserva uma tupla somente leitura, uma lista com posições conhecidas: readonly ["email", "whatsapp", "sms"]. A consulta de tipo typeof canais recupera o tipo dessa variável. O acesso [number] reúne os tipos de seus elementos, formando "email" | "whatsapp" | "sms".
A restrição readonly string[] permite acomodar essa tupla. O parâmetro const preserva informação durante a inferência, sem congelar o valor em tempo de execução. Se uma variável anterior já tiver sido inferida como string[], passá-la à função não recuperará os literais perdidos.
Use NoInfer para definir de onde vem a inferência
NoInfer<T>, disponível desde o TypeScript 5.4, impede que uma posição contribua para a inferência de T. O valor nessa posição continua sendo verificado contra o tipo encontrado. Em um seletor, a lista pode definir os canais permitidos, enquanto a opção padrão precisa pertencer a ela.
function selecionarCanal<const T extends readonly string[]>(
opcoes: T,
padrao: NoInfer<T[number]>,
): T[number] {
if (opcoes.indexOf(padrao) === -1) {
throw new Error("O canal padrão precisa estar na lista");
}
return padrao;
}
const canal = selecionarCanal(["email", "whatsapp"], "email");
// ^?
// erro esperado: TS2345
selecionarCanal(["email", "whatsapp"], "sms");- linha 11
const canal: "email" | "whatsapp" - linha 15
TS2345: Argument of type '"sms"' is not assignable to parameter of type '"email" | "whatsapp"'.
A lista determina T, e T[number] representa seus canais. NoInfer mantém o argumento padrao fora dessa decisão: ele precisa caber no tipo já obtido. A última chamada produz TS2345 porque "sms" não pertence a "email" | "whatsapp".
Neste exemplo, a restrição em forma de lista já contribui para uma inferência precisa. NoInfer também registra no contrato qual argumento deve orientar a escolha do tipo. Essa intenção fica especialmente útil quando a assinatura cresce e outras entradas passam a compartilhar o mesmo parâmetro.
Modele interfaces genéricas e parâmetros padrão
Uma interface descreve a estrutura exigida de um valor. Em uma interface genérica, o parâmetro de tipo pode aparecer em propriedades, argumentos e retornos de métodos. Repositorio<T> relaciona o tipo salvo ao tipo buscado, enquanto Resposta<T> descreve o conteúdo entregue.
interface Repositorio<T> {
buscar(id: string): T | undefined;
salvar(item: T): void;
}
interface Resposta<T = unknown> {
dados: T;
origem: string;
}
function carregar<T>(
repositorio: Repositorio<T>,
id: string,
): Resposta<T | undefined> {
return {
dados: repositorio.buscar(id),
origem: "Mirante",
};
}
const resposta: Resposta<{ totalLeads: number }> = {
dados: { totalLeads: 42 },
origem: "painel",
};
const total = resposta.dados.totalLeads;
// ^?
const respostaSemContrato: Resposta = {
dados: { campanha: "outono" },
origem: "integração",
};
const dados = respostaSemContrato.dados;
// ^?- linha 26
const total: number - linha 34
const dados: unknown - resultado
sem erro de tipo, com strict
Em Resposta<T = unknown>, unknown é o parâmetro de tipo padrão, usado quando nenhum argumento de tipo é informado. Esse tipo aceita qualquer valor, mas exige verificação antes de acessar suas propriedades. Por isso, dados permanece unknown em respostaSemContrato, mesmo que o objeto atribuído tenha uma propriedade campanha.
O retorno void de salvar indica que o contrato não oferece um resultado para uso pelo chamador. Já buscar pode devolver undefined quando o identificador estiver ausente. A interface descreve essas operações; cada implementação decide como guardar e recuperar os dados.
A função carregar infere T a partir do repositório recebido. Um Repositorio<Lead> produz Resposta<Lead | undefined>, e um repositório de campanhas produz a resposta correspondente. Se o contrato permitir apenas registros identificáveis, a declaração pode receber uma restrição como Repositorio<T extends { id: string }>.
Separe sucesso e falha com Result<T, E>
Uma API, interface de comunicação entre sistemas, pode devolver dados ou uma falha que a aplicação precisa apresentar. Result<T, E> modela essas alternativas usando uma união discriminada: variantes identificadas por uma propriedade comum com valores literais diferentes. Aqui, ok permite ao compilador distinguir sucesso de erro.
A chamada assíncrona, que aguarda uma operação sem produzir o resultado imediatamente, retorna uma Promise, representação de um resultado futuro. O corpo recebido em JSON, formato textual de dados estruturados, começa como unknown. A validação confirma as propriedades antes de construir o lead.
type Lead = { id: string; email: string };
type ErroApi = {
codigo: "consulta" | "formato" | "leitura";
mensagem: string;
};
type Result<T, E> =
| { ok: true; valor: T }
| { ok: false; erro: E };
async function buscarLead(id: string): Promise<Result<Lead, ErroApi>> {
try {
const resposta = await fetch(`/api/leads/${encodeURIComponent(id)}`);
if (!resposta.ok) {
return { ok: false, erro: {
codigo: "consulta", mensagem: "Não foi possível consultar o lead",
} };
}
const corpo: unknown = await resposta.json();
if (
typeof corpo !== "object" || corpo === null ||
!("id" in corpo) || typeof corpo.id !== "string" ||
!("email" in corpo) || typeof corpo.email !== "string"
) {
return { ok: false, erro: {
codigo: "formato", mensagem: "A integração enviou dados inválidos",
} };
}
return { ok: true, valor: { id: corpo.id, email: corpo.email } };
} catch {
return { ok: false, erro: {
codigo: "leitura", mensagem: "Falha ao receber ou ler a resposta",
} };
}
}
async function atualizarPainel(id: string): Promise<string> {
const resultado = await buscarLead(id);
if (resultado.ok) return resultado.valor.email;
return resultado.erro.mensagem;
}- resultado
sem erro de tipo, com strict
O teste resultado.ok faz o estreitamento de tipo, processo que reduz as possibilidades conforme uma condição. No ramo de sucesso, resultado.valor está disponível como Lead. No caminho de falha, o código acessa resultado.erro como ErroApi.
O genérico relaciona os dados e os erros declarados pela função. A validação do exemplo confere a estrutura mínima, com id e email textuais; a validade do endereço de e-mail exige uma regra adicional. Para integrar contratos maiores, uma biblioteca de validação pode assumir essa checagem antes da construção do resultado.
Derive tipos do lead e da configuração
Uma definição, vários usos
Tipos utilitários são transformações que produzem um tipo a partir de outro. Na Mirante, o cadastro completo de um lead serve de base para rascunhos, formulários e painéis. Cada uso seleciona ou modifica os campos necessários, mantendo a definição original como referência.
Essas transformações atuam na verificação do TypeScript. Elas deixam o código JavaScript responsável por criar objetos, remover campos e conferir dados recebidos. Uma propriedade opcional, por exemplo, é um campo que pode estar ausente do objeto.
Quatorze utilitários para consultar
Na tabela, T representa o tipo de origem e K, as chaves escolhidas, ou seja, os nomes das propriedades. Uma união reúne alternativas de tipo, como "novo" | "qualificado". Um literal representa um valor específico, como o texto "captura".
| Nome | O que faz | Exemplo da agência |
|---|---|---|
Partial<T> | Torna todas as propriedades opcionais | Partial<LeadNovo> representa um rascunho de lead |
Required<T> | Torna todas as propriedades obrigatórias | Required<Pick<Lead, "telefone">> exige telefone no contato |
Readonly<T> | Impede atribuições às propriedades pelo tipo | Readonly<ResumoLead> protege os campos exibidos no painel |
Pick<T, K> | Seleciona propriedades | Pick<Lead, "nome" | "status"> define o resumo do lead |
Omit<T, K> | Exclui propriedades | Omit<Lead, "id" | "criadoEm"> define os campos de um lead novo |
Record<K, T> | Cria um objeto com chaves K e valores do tipo T | Record<Canal, number> distribui o orçamento em centavos |
Exclude<T, U> | Retira de uma união os membros compatíveis com U | Exclude<Canal, "email"> seleciona canais sem email |
Extract<T, U> | Mantém os membros de uma união compatíveis com U | Extract<Canal, "busca" | "social"> seleciona canais de mídia |
NonNullable<T> | Retira null e undefined de um tipo | NonNullable<RespostaLead["email"]> representa um email presente |
ReturnType<T> | Obtém o tipo retornado por uma função | ReturnType<typeof carregarLead> acompanha o retorno da consulta |
Parameters<T> | Obtém os parâmetros da função como tupla, uma sequência de posições tipadas | Parameters<typeof carregarLead> define os argumentos da consulta |
Awaited<T> | Obtém o resultado resolvido de uma operação assíncrona | Awaited<ReturnType<typeof carregarLead>> representa o lead carregado |
Uppercase<T> | Converte um tipo literal de texto para maiúsculas | Uppercase<"mirante"> produz o tipo "MIRANTE" |
Lowercase<T> | Converte um tipo literal de texto para minúsculas | Lowercase<"CAPTURA"> produz o tipo "captura" |
Do cadastro completo ao rascunho
LeadNovo exclui os campos que o sistema gera ao cadastrar o lead. Partial<LeadNovo> permite salvar o formulário ainda incompleto, enquanto Required pode exigir os campos de uma etapa específica. O acesso Lead["status"], chamado acesso indexado de tipo, recupera diretamente o tipo daquele campo.
type Lead = {
id: string;
criadoEm: Date;
nome: string;
email: string;
telefone?: string;
status: "novo" | "qualificado" | "descartado";
};
type LeadNovo = Omit<Lead, "id" | "criadoEm">;
type ResumoLead = Pick<Lead, "nome" | "status">;
type StatusLead = Lead["status"];
const rascunho: Partial<LeadNovo> = { nome: "Ana" };
// ^?
const contato: Required<Pick<Lead, "email" | "telefone">> = {
email: "ana@example.com",
telefone: "11999990000",
};
// ^?
const painel: Readonly<ResumoLead> = {
nome: "Ana",
status: "novo",
};
// erro esperado: TS2540
painel.status = "qualificado";- linha 14
const rascunho: Partial<LeadNovo> - linha 17
const contato: Required<Pick<Lead, "email" | "telefone">> - linha 29
TS2540: Cannot assign to 'status' because it is a read-only property.
A última atribuição gera o erro indicado porque Readonly impede reatribuir status por essa referência. Essa proteção existe na checagem de tipos; o objeto continua sendo um objeto comum durante a execução. Tanto Readonly quanto Partial atuam somente no primeiro nível: objetos internos mantêm suas próprias regras.
Required exige a presença dos campos, mas não confere se o telefone está preenchido ou segue o formato esperado. Omit também só transforma o tipo, sem apagar propriedades de um objeto existente. Dados recebidos de formulários precisam de validação em execução, a conferência dos valores enquanto o programa roda.
Use a configuração como fonte das chaves
Quando a configuração já lista os canais disponíveis, keyof typeof permite derivar seus nomes. Nesse contexto, typeof obtém o tipo de um valor, e keyof reúne as chaves desse tipo. Com Record, cada canal passa a exigir uma entrada no orçamento.
A expressão as const preserva os valores literais e marca as propriedades como somente leitura no tipo. Em uma lista literal, ela também preserva cada posição como parte de uma tupla. O acesso Array[number] extrai o tipo dos elementos de um array, uma lista de valores; no exemplo, a forma concreta é (typeof origens)[number].
const canais = {
busca: { nome: "Busca paga" },
social: { nome: "Redes sociais" },
email: { nome: "Email" },
} as const;
type Canal = keyof typeof canais;
type CanalSemEmail = Exclude<Canal, "email">;
type CanalDeMidia = Extract<Canal, "busca" | "social">;
const orcamento: Record<Canal, number> = {
busca: 120000,
social: 80000,
email: 20000,
};
// ^?
const canalPrincipal: CanalDeMidia = "busca";
// ^?
const origens = ["site", "evento"] as const;
type Origem = (typeof origens)[number];
const origem: Origem = "site";
// ^?- linha 11
const orcamento: Record<"busca" | "social" | "email", number> - linha 18
const canalPrincipal: CanalDeMidia - linha 24
const origem: "site" | "evento" - resultado
sem erro de tipo, com strict
Adicionar uma chave a canais faz o orçamento precisar dessa chave também. Exclude e Extract trabalham com os membros da união, enquanto Omit e Pick trabalham com propriedades de objetos. No exemplo atual, CanalSemEmail e CanalDeMidia resultam em "busca" | "social", embora expressem critérios diferentes.
Sem as const, a lista origens seria inferida como string[], um array de textos quaisquer. Nesse caso, o acesso por [number] produziria string. Preserve os literais quando a lista representar um conjunto fechado de opções.
Derive os argumentos e o resultado das funções
ReturnType e Parameters acompanham a assinatura de uma função, isto é, seus parâmetros e seu retorno. Isso ajuda a criar integrações que reutilizam uma consulta sem copiar manualmente seu contrato. Em funções assíncronas, ReturnType inclui a Promise, o objeto que representa um resultado futuro, e Awaited extrai o resultado resolvido.
async function carregarLead(id: string) {
const email: string | null = id === "" ? null : "ana@example.com";
return { id, email };
}
type TarefaLead = ReturnType<typeof carregarLead>;
type RespostaLead = Awaited<TarefaLead>;
type EmailPresente = NonNullable<RespostaLead["email"]>;
const argumentos: Parameters<typeof carregarLead> = ["lead-42"];
// ^?
const tarefa = carregarLead(...argumentos);
// ^?
function prepararContato(resposta: RespostaLead): string {
if (resposta.email === null) {
return "Lead sem email";
}
const email: EmailPresente = resposta.email;
// ^?
return email.toLowerCase();
}- linha 10
const argumentos: [id: string] - linha 13
const tarefa: Promise<{ id: string; email: string | null; }> - linha 21
const email: string - resultado
sem erro de tipo, com strict
O operador ... espalha os argumentos da tupla na chamada da função. A condição dentro de prepararContato faz um estreitamento de tipo: depois de descartar null, o TypeScript reconhece o email como texto. NonNullable descreve o tipo desejado, mas a condição é que verifica o valor recebido.
Essa distinção também aparece em Uppercase e Lowercase: eles transformam tipos de texto. Para converter o conteúdo real de uma variável, use operações como toUpperCase() e toLowerCase(). Reserve os utilitários para expressar contratos e mantenha as verificações necessárias no código executado.
Tipos que derivam campos, erros e caminhos
Um formulário da Mirante pode compartilhar campos com mensagens de erro, funções de leitura e atualizações parciais. Tipos derivados (tipos calculados a partir de outros) ajudam a manter esses contratos alinhados. Cada transformação acrescenta trabalho para o compilador (a ferramenta que verifica o código), então convém manter a regra perto do problema que ela resolve.
Tipos condicionais: escolher pela compatibilidade
Um tipo condicional escolhe um resultado conforme a compatibilidade entre dois tipos: T extends U ? X : Y. Nessa expressão, T é um parâmetro genérico (um tipo recebido por outro tipo), e extends pergunta se ele pode ser atribuído a U. O compilador resolve a escolha durante a verificação, sem executar uma condição no JavaScript.
type CanalDoLead<T> =
T extends { email: string } ? "email" : "telefone";
type LeadComEmail = { nome: string; email: string };
type LeadComTelefone = { nome: string; telefone: string };
const principal: CanalDoLead<LeadComEmail> = "email";
// ^?
const alternativo: CanalDoLead<LeadComTelefone> = "telefone";
// ^?- linha 7
const principal: "email" - linha 9
const alternativo: "telefone" - resultado
sem erro de tipo, com strict
A verificação considera a estrutura (os campos e seus tipos) de LeadComEmail. Por isso, o campo extra nome permite que a condição continue verdadeira. O ramo "telefone" funciona como alternativa do exemplo, mas a condição sozinha não comprova que exista um campo telefone: um tipo apenas com nome também cairia nele.
infer: capturar uma parte do tipo
A palavra infer declara um tipo a ser extraído dentro da condição. Ela permite reconhecer um array (lista de itens) e capturar o tipo de seus elementos. Também permite capturar o tipo do resultado de uma Promise (objeto que representa um resultado assíncrono).
type ItemDoArray<T> =
T extends readonly (infer Item)[] ? Item : never;
type ResultadoDaPromise<T> =
T extends Promise<infer Resultado> ? Resultado : T;
type Lead = {
nome: string;
email: string;
};
const exemplo: ItemDoArray<readonly Lead[]> = {
nome: "Ana",
email: "ana@mirante.example",
};
// ^?
const total: ResultadoDaPromise<Promise<number>> = 320;
// ^?- linha 12
const exemplo: Lead - linha 17
const total: number - resultado
sem erro de tipo, com strict
Em ItemDoArray, readonly permite reconhecer tanto listas mutáveis quanto listas somente para leitura. O resultado never representa a ausência de valores possíveis quando a entrada não corresponde ao padrão. Já ResultadoDaPromise devolve o próprio T quando ele não corresponde a uma Promise, e extrai apenas uma camada quando corresponde.
Uniões: distribuir a condição ou avaliar o conjunto
Uma união representa alternativas de tipo, como LeadComEmail | LeadSemEmail. Quando o lado esquerdo de extends é diretamente um parâmetro genérico, a condição é distributiva: ela avalia cada alternativa separadamente. Envolver os dois lados em tuplas (listas com posições definidas), como [T] extends [U], suprime essa distribuição.
type LeadComEmail = { nome: string; email: string };
type LeadSemEmail = { nome: string; telefone: string };
type Entrada = LeadComEmail | LeadSemEmail;
type FiltrarComEmail<T> =
T extends { email: string } ? T : never;
type TodosPossuemEmail<T> =
[T] extends [{ email: string }] ? true : false;
const selecionado: FiltrarComEmail<Entrada> = {
nome: "Bia",
email: "bia@mirante.example",
};
// ^?
const todos: TodosPossuemEmail<Entrada> = false;
// ^?- linha 11
const selecionado: LeadComEmail - linha 16
const todos: false - resultado
sem erro de tipo, com strict
FiltrarComEmail<Entrada> calcula LeadComEmail | never, que resulta em LeadComEmail. Já TodosPossuemEmail<Entrada> avalia se a união inteira pode ser atribuída ao contrato com email, produzindo false. Essa diferença importa ao filtrar eventos de campanha ou exigir que todas as respostas de uma integração tenham determinado campo.
Tipos mapeados: transformar cada propriedade
Um tipo mapeado percorre as propriedades de outro tipo e produz um novo contrato. keyof T obtém a união das chaves, [Campo in keyof T] percorre essas chaves, e T[Campo] consulta o tipo de cada propriedade. Os modificadores controlam se os campos aceitam alteração e se podem ser omitidos.
-readonly: remove a restrição de somente leitura.-?: remove a marca de propriedade opcional.+?: torna a propriedade opcional; escrever apenas?tem o mesmo efeito.
type EditavelCompleto<T> = {
-readonly [Campo in keyof T]-?: T[Campo];
};
type CamposOpcionais<T> = {
[Campo in keyof T]+?: T[Campo];
};
type Campanha = {
readonly nome: string;
readonly orcamentoCentavos?: number;
};
const campanha: EditavelCompleto<Campanha> = {
nome: "Captação Mirante",
orcamentoCentavos: 150000,
};
// ^?
campanha.orcamentoCentavos = 180000;
const rascunho: CamposOpcionais<Campanha> = {};- linha 14
const campanha: EditavelCompleto<Campanha> - resultado
sem erro de tipo, com strict
EditavelCompleto exige todos os campos e permite atribuir novos valores às propriedades. CamposOpcionais permite omitir campos, mas preserva o readonly que já existia no tipo original. Essas transformações alteram o contrato estático (as regras verificadas antes da execução), sem copiar ou modificar objetos existentes.
Remapear chaves com as: getters e erros por campo
O remapeamento de chaves permite trocar o nome de cada propriedade durante o mapeamento, usando as. Um tipo de template literal (texto de tipo construído com interpolações) combina prefixos com nomes de campos. Capitalize é um utilitário que transforma a primeira letra de um texto de tipo em maiúscula.
type Getters<T> = {
[Campo in keyof T as
Campo extends string ? `get${Capitalize<Campo>}` : never]:
() => T[Campo];
};
type ErrosDoFormulario<T> = {
[Campo in keyof T as
Campo extends string ? `erro${Capitalize<Campo>}` : never]-?:
string;
};
type Formulario = {
nome: string;
email: string;
};
const getters: Getters<Formulario> = {
getNome: () => "Mirante",
getEmail: () => "contato@mirante.example",
};
// ^?
const erros: ErrosDoFormulario<Formulario> = {
erroNome: "Informe o nome.",
erroEmail: "Informe um e-mail válido.",
};
// ^?- linha 18
const getters: Getters<Formulario> - linha 23
const erros: ErrosDoFormulario<Formulario> - resultado
sem erro de tipo, com strict
O teste Campo extends string seleciona chaves textuais, que podem entrar na composição dos nomes. Quando o remapeamento produz never, aquela chave fica fora do resultado. Em ErrosDoFormulario<T>, -? exige uma mensagem para cada campo textual, inclusive quando o campo original for opcional.
Esse contrato de erros serve, por exemplo, para um catálogo completo de mensagens do formulário. Para armazenar somente os erros presentes após uma validação, troque -? por +?, permitindo omitir as demais mensagens. Os getters (funções que leem valores) ainda precisam de implementação: o tipo descreve suas assinaturas, sem criar funções durante a execução.
Recursão: aplicar DeepPartial em objetos aninhados
Um tipo recursivo referencia a si mesmo para continuar uma transformação em níveis internos. DeepPartial torna opcionais as propriedades do objeto e repete essa regra dentro de cada campo. A condição precisa de um caso de parada, que aqui preserva valores como textos e números.
type DeepPartial<T> =
T extends readonly (infer Item)[]
? ReadonlyArray<DeepPartial<Item>>
: T extends object
? { [Campo in keyof T]?: DeepPartial<T[Campo]> }
: T;
type Lead = {
nome: string;
endereco: {
cidade: string;
cep: string;
};
interesses: {
nome: string;
prioridade: number;
}[];
};
const alteracao: DeepPartial<Lead> = {
endereco: { cidade: "Recife" },
interesses: [{ nome: "Design" }],
};
// ^?- linha 20
const alteracao: { nome?: string | undefined; endereco?: { cidade?: string | undefined; cep?: string | undefined; } | undefined; interesses?: readonly { nome?: string | undefined; prioridade?: number | undefined; }[] | undefined; } - resultado
sem erro de tipo, com strict
A primeira condição trata listas antes dos demais objetos e aplica a transformação aos itens. Essa implementação escolhe devolver listas somente para leitura, por meio de ReadonlyArray, mesmo quando a lista original permite alterações. O ramo seguinte percorre objetos, enquanto o último encerra a recursão nos valores restantes.
Use essa versão com dados formados por objetos simples, listas e valores básicos. Funções, datas e coleções especializadas pedem regras próprias, pois também podem corresponder a object. O contrato permite atualizações parciais, mas a aplicação ainda precisa definir como combinar os dados e se uma lista recebida substitui a anterior.
Recursão para caminhos como endereco.cidade
Um caminho textual identifica um campo aninhado por uma sequência de chaves separadas por ponto. Para produzi-lo, o tipo mapeado inclui cada chave atual e combina essa chave com os caminhos dos níveis internos. Ao final, um acesso indexado (consulta de propriedades por suas chaves) reúne os resultados em uma união.
type Caminhos<T> =
T extends object
? {
[Campo in Extract<keyof T, string>]-?:
Campo | `${Campo}.${Caminhos<T[Campo]>}`;
}[Extract<keyof T, string>]
: never;
type Lead = {
nome: string;
endereco: {
cidade: string;
cep: string;
};
};
const caminho: Caminhos<Lead> = "endereco.cidade";
// ^?
const escolherCampo = (campo: Caminhos<Lead>) => campo;
// ^?
// erro esperado: TS2322
const invalido: Caminhos<Lead> = "endereco.bairro";- linha 17
const caminho: "nome" | "endereco" | "endereco.cidade" | "endereco.cep" - linha 19
const escolherCampo: (campo: Caminhos<Lead>) => "nome" | "endereco" | "endereco.cidade" | "endereco.cep" - linha 23
TS2322: Type '"endereco.bairro"' is not assignable to type '"nome" | "endereco" | "endereco.cidade" | "endereco.cep"'.
Extract<keyof T, string> seleciona apenas as chaves compatíveis com texto. Para esse Lead, o resultado reúne "nome", "endereco", "endereco.cidade" e "endereco.cep". A variável caminho aparece com seu literal específico, pois o compilador conhece o valor atribuído; a assinatura de escolherCampo mostra as alternativas aceitas.
Essa implementação atende a objetos simples aninhados e inclui tanto os campos intermediários quanto os finais. Listas, funções e chaves que já contêm ponto exigem outra política de caminhos. O tipo verifica o texto fornecido, enquanto a leitura ou escrita do valor correspondente precisa de uma função própria.
Quando parar: legibilidade, mensagens de erro e TS2589
Uma transformação merece continuar quando sua regra cabe numa explicação curta e evita repetição real nos contratos. Observe também a mensagem apresentada quando alguém usa o tipo incorretamente. Se um erro simples de campo produz várias camadas de detalhes difíceis de relacionar ao formulário, divida a transformação em tipos nomeados ou prefira um contrato explícito.
- Legibilidade: outra pessoa consegue prever o resultado sem simular toda a recursão?
- Mensagens de erro: o diagnóstico aponta o campo ou expõe principalmente os detalhes internos do utilitário?
- Domínio: o tipo trata apenas os dados usados pela agência ou tenta cobrir qualquer estrutura possível?
- Custo de manutenção: alterar um campo exige compreender várias transformações encadeadas?
O erro TS2589, “Type instantiation is excessively deep and possibly infinite”, indica que a expansão dos tipos chegou ao limite de profundidade aceito pelo compilador. Ele pode surgir ao combinar recursão, uniões grandes e várias transformações, mesmo sem uma recursão infinita intencional. Reduza o trabalho por etapa, restrinja os tipos de entrada ou adote um limite explícito de profundidade.
Em caminhos de formulário, uma alternativa é manter uma união explícita apenas dos campos que a interface realmente edita. Para atualizações, contratos específicos por operação podem deixar mais claro quais alterações são permitidas. Evite esconder o TS2589 com any (tipo que dispensa a verificação de compatibilidade), pois isso elimina justamente as garantias procuradas pelo utilitário.
Combine nomes com template literal types
Classes de botão com combinações permitidas
Um template literal type (tipo que descreve padrões de texto) combina trechos fixos com tipos entre ${...}. A sintaxe lembra as strings com interpolação do JavaScript, mas aqui descreve valores aceitos pelo compilador (programa que verifica os tipos). Uma união (tipo que aceita uma entre várias alternativas) fornece as opções de cada trecho.
Na Mirante, os botões de captura de leads podem seguir um conjunto fechado de tamanhos e variantes. Cada opção de tamanho combina com cada opção de variante. O resultado permite conferir os nomes de classes antes de publicar o formulário.
type Tamanho = "pequeno" | "medio" | "grande";
type Variante = "primario" | "secundario";
type ClasseBotao = `btn-${Tamanho}-${Variante}`;
const classe = "btn-pequeno-primario" satisfies ClasseBotao;
// ^?
// erro esperado: TS2820 (a forma do TS2322 que sugere o nome mais parecido)
const classeInvalida: ClasseBotao = "btn-enorme-primario";- linha 5
const classe: "btn-pequeno-primario" - linha 9
TS2820: Type '"btn-enorme-primario"' is not assignable to type '"btn-pequeno-primario" | "btn-pequeno-secundario" | "btn-medio-primario" | "btn-medio-secundario" | "btn
ClasseBotao contém seis combinações, como "btn-medio-secundario" e "btn-grande-primario". O operador satisfies (verificação de compatibilidade que preserva a inferência da expressão) confere a classe escolhida. Inferência é o cálculo de um tipo a partir do código, e o marcador ^? mostra esse resultado no laboratório. A verificação garante a grafia permitida; a existência da classe na folha de estilos continua dependendo do projeto.
Eventos com prefixo e inicial maiúscula
Os tipos utilitários de texto (transformações prontas para tipos de string) ajudam a padronizar nomes. Capitalize<Texto> coloca o primeiro caractere em maiúscula. Com ele, os eventos "enviar" e "cancelar" podem gerar propriedades chamadas onEnviar e onCancelar.
type Evento = "enviar" | "cancelar";
type NomeEvento = `on${Capitalize<Evento>}`;
type Ouvintes = {
[Nome in NomeEvento]: () => void;
};
const formulario: Ouvintes = {
onEnviar: () => console.log("Lead enviado"),
onCancelar: () => console.log("Captura cancelada"),
};
// ^?- linha 8
const formulario: Ouvintes - resultado
sem erro de tipo, com strict
O tipo mapeado (tipo que cria propriedades percorrendo um conjunto de chaves) aparece em [Nome in NomeEvento]. Cada propriedade recebe uma função () => void, isto é, uma função sem argumentos e sem valor de retorno utilizado. Se um novo evento entrar na união, o objeto formulario também precisará oferecer a função correspondente.
Quatro transformações de texto
Além de Capitalize, o TypeScript oferece outras três transformações. Todas operam sobre tipos, durante a verificação do código. Para transformar uma string recebida de um formulário, use uma operação em tempo de execução (quando o programa está rodando).
| Utilitário | Entrada | Resultado |
|---|---|---|
Uppercase<Texto> | "leadNovo" | "LEADNOVO" |
Lowercase<Texto> | "LeadNovo" | "leadnovo" |
Capitalize<Texto> | "leadNovo" | "LeadNovo" |
Uncapitalize<Texto> | "LeadNovo" | "leadNovo" |
type NomeBase = "campanhaMirante";
const destaque: Uppercase<NomeBase> = "CAMPANHAMIRANTE";
// ^?
const busca: Lowercase<NomeBase> = "campanhamirante";
const componente: Capitalize<NomeBase> = "CampanhaMirante";
const propriedade: Uncapitalize<"CampanhaMirante"> = "campanhaMirante";
// ^?- linha 3
const destaque: "CAMPANHAMIRANTE" - linha 7
const propriedade: "campanhaMirante" - resultado
sem erro de tipo, com strict
Capitalize e Uncapitalize alteram apenas o primeiro caractere. Elas preservam o restante do nome, inclusive maiúsculas internas como o M de "campanhaMirante". Isso ajuda a derivar nomes de propriedades e eventos a partir de uma mesma definição.
Atributos com prefixo aberto
Uma união fecha as alternativas possíveis; string abre espaço para qualquer texto naquela posição. O padrão `data-${string}` aceita nomes como data-campanha e data-origem. Ele serve para modelar atributos personalizados do HTML (marcações que armazenam dados no elemento).
type AtributosDados = {
[Chave in `data-${string}`]?: string;
};
const atributos = {
"data-campanha": "captura-mirante",
"data-origem": "painel",
} satisfies AtributosDados;
// ^?- linha 5
const atributos: { "data-campanha": string; "data-origem": string; } - resultado
sem erro de tipo, com strict
O ? torna cada propriedade opcional: o objeto pode fornecer apenas os atributos necessários. O prefixo fica controlado, enquanto o sufixo permanece livre. Esse padrão também aceita "data-", pois string inclui o texto vazio; regras adicionais exigem uma validação própria.
Rotas com partes variáveis
Uma rota (caminho usado para acessar um recurso) também pode seguir um padrão de texto. Para os leads das campanhas da Mirante, o tipo `/campanhas/${string}/leads` fixa o começo e o final do caminho. Uma função pode receber esse tipo para reduzir erros de digitação nas chamadas.
type RotaLeads = `/campanhas/${string}/leads`;
function abrirPainel(rota: RotaLeads): string {
return `Abrindo ${rota}`;
}
const rota = "/campanhas/lancamento/leads" satisfies RotaLeads;
// ^?
abrirPainel(rota);
// erro esperado: TS2322
const rotaInvalida: RotaLeads = "/campanhas/lancamento/contatos";- linha 7
const rota: "/campanhas/lancamento/leads" - linha 12
TS2322: Type '"/campanhas/lancamento/contatos"' is not assignable to type '`/campanhas/${string}/leads`'.
O trecho ${string} aceita texto vazio e também barras. Assim, esse tipo sozinho permite tanto "/campanhas//leads" quanto "/campanhas/a/b/leads". Para exigir um identificador preenchido e sem barras, valide o valor antes de montar a rota.
A anotação de tipo também não confirma que a campanha existe ou que o usuário pode acessá-la. Dados recebidos pela rede precisam de validação durante a execução. A autorização continua sendo responsabilidade do código que atende à requisição.
Extraia parâmetros com infer
Um tipo condicional (tipo que escolhe um resultado conforme uma condição) consegue reconhecer partes de uma string. Dentro dessa condição, infer declara uma variável de tipo para capturar um trecho correspondente ao padrão. Isso permite converter "/leads/:id" em { id: string }.
O exemplo abaixo considera parâmetros escritos como segmentos :nome, separados por /. Ele não trata parâmetros opcionais nem a parte de consulta da URL (endereço de um recurso). Essa convenção mantém a extração pequena e previsível.
type ParametrosRota<Caminho extends string> =
Caminho extends `${string}:${infer Nome}/${infer Resto}`
? { [Chave in Nome | keyof ParametrosRota<Resto>]: string }
: Caminho extends `${string}:${infer Nome}`
? { [Chave in Nome]: string }
: {};
const parametros: ParametrosRota<"/leads/:id"> = {
id: "lead-42",
};
// ^?
const filtros: ParametrosRota<"/campanhas/:campanhaId/leads/:id"> = {
campanhaId: "campanha-7",
id: "lead-42",
};
// ^?- linha 8
const parametros: { id: string; } - linha 13
const filtros: { id: string; campanhaId: string; } - resultado
sem erro de tipo, com strict
Caminho extends string limita o parâmetro genérico (tipo recebido por outro tipo) a strings. No primeiro ramo, infer Nome captura o nome anterior à próxima barra, e infer Resto captura o restante. A recursão (aplicação do mesmo tipo a uma parte menor da entrada) procura os próximos parâmetros.
O operador keyof obtém as chaves encontradas no restante da rota. A união dessas chaves com Nome forma as propriedades do objeto resultante, todas com valor string. No último ramo, sem parâmetros encontrados, {} não acrescenta nenhuma chave à extração.
Esse mecanismo descreve o formato dos parâmetros, mas não extrai valores de uma URL recebida. Uma função de leitura de rotas ainda precisa produzir o objeto durante a execução. O tipo pode então conferir se essa função e seus consumidores concordam sobre os nomes esperados.
Controle a explosão de combinações
Cada união interpolada multiplica o total de alternativas. Três tamanhos e duas variantes produzem seis membros; dez opções em cinco posições produzem 100.000. Essa multiplicação é a explosão combinatória (crescimento acelerado do número de combinações).
O compilador tem um limite de representação para essas uniões. Uma expansão que chegaria a 100.000 membros atinge esse limite e gera TS2590, indicando que a união é complexa demais para ser representada. O exemplo mantém duas posições válidas e mostra o erro esperado ao tentar expandir cinco.
type Digito = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
type Prefixo = `${Digito}${Digito}`;
const prefixo: Prefixo = "42";
// ^?
// erro esperado: TS2590
type Codigo = `${Digito}${Digito}${Digito}${Digito}${Digito}`;- linha 4
const prefixo: "00" | "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | "20" | "21" | "22" | "23" | "24" | "25" | "26" | "27" | "28" | "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36" | "37" | "38" | "39" | "40" | "41" | "42" | "43" | "44" | "45" | "46" | "47" | "48" | "49" | "50" | "51" | "52" | "53" | "54" | "55" | "56" | "57" | "58" | "59" | "60" | "61" | "62" | "63" | "64" | "65" | "66" | "67" | "68" | "69" | "70" | "71" | "72" | "73" | "74" | "75" | "76" | "77" | "78" | "79" | "80" | "81" | "82" | "83" | ... 15 more ... | "99" - linha 8
TS2590: Expression produces a union type that is too complex to represent.
Para códigos de campanha, identificadores externos e outros espaços grandes de valores, evite enumerar todas as combinações no sistema de tipos. Prefira manter o prefixo útil, como `campanha-${string}`, e validar comprimento e caracteres durante a execução. Uniões pequenas funcionam melhor para escolhas controladas, como tamanho, variante e nome de evento.
Conferir sem apagar: satisfies, as const e a saída do enum
Anotação versus satisfies
Quando você escreve const x: Tipo = ... (anotação), o TypeScript passa a enxergar a variável só pelo contrato. O que você digitou de específico some. O operador satisfies faz a conferência contra o mesmo contrato, mas deixa a variável com o tipo inferido (o tipo que o compilador deduz sozinho a partir do valor).
Nos tokens de design dos botões da Mirante, essa diferença decide se o autocompletar conhece os nomes das variantes:
type Tamanho = "pequeno" | "medio" | "grande";
type Variante = { fundo: string; altura: number; tamanho: Tamanho };
// Anotação: o contrato vira o tipo e os nomes das variantes somem
const anotados: Record<string, Variante> = {
primario: { fundo: "#0f62fe", altura: 40, tamanho: "medio" },
};
type NomeAnotado = keyof typeof anotados;
// ^?
// satisfies: confere o contrato e guarda o que foi escrito
const botoes = {
primario: { fundo: "#0f62fe", altura: 40, tamanho: "medio" },
fantasma: { fundo: "transparent", altura: 32, tamanho: "pequeno" },
} satisfies Record<string, Variante>;
type NomeBotao = keyof typeof botoes;
// ^?
const tam = botoes.fantasma.tamanho;
// ^?- linha 8
type NomeAnotado = string - linha 16
type NomeBotao = "primario" | "fantasma" - linha 18
const tam: "pequeno" - resultado
sem erro de tipo, com strict
Com a anotação, as chaves viram string | number, e anotados.qualquerCoisa compila sem reclamar. Com satisfies, as chaves são "primario" | "fantasma" e o tamanho do botão fantasma continua "pequeno". Se alguém escrever tamanho: "enorme" ou esquecer altura, o erro aparece do mesmo jeito.
| Situação | Use |
|---|---|
| Parâmetro de função, retorno de API, variável que vai mudar | Anotação : Tipo |
| Objeto de configuração fixo (tokens, rotas, mapa de campos) | satisfies Tipo |
| Configuração fixa que também precisa de valores literais e somente leitura | as const satisfies Tipo |
as const para canais e passos do funil
Um array comum de strings é inferido como string[]. Ele aceita qualquer texto e o tamanho pode mudar. A asserção as const diz ao compilador que o valor é fixo. O array vira uma tupla somente leitura (lista de tamanho e posições conhecidos), com cada item no seu tipo literal.
const soltos = ["email", "whatsapp"];
// ^?
const canais = ["email", "whatsapp", "sms"] as const;
type Canal = (typeof canais)[number];
// ^?
const passos = ["captura", "nutricao", "oferta", "venda"] as const;
type Passo = (typeof passos)[number];
// A ordem do funil vem da própria lista
function proximo(passo: Passo): Passo | undefined {
const i = passos.indexOf(passo);
return passos[i + 1];
}- linha 1
const soltos: string[] - linha 5
type Canal = "email" | "whatsapp" | "sms" - resultado
sem erro de tipo, com strict
O truque (typeof canais)[number] lê o tipo de qualquer posição da tupla e devolve a união "email" | "whatsapp" | "sms". A lista passa a ser a única fonte: quem incluir "telegram" no array ganha o canal novo no tipo, sem editar outra declaração.
Objeto as const no lugar de enum
O enum existe só no TypeScript e gera código. enum StatusLead { Novo = "novo" } vira, no JavaScript emitido, uma função que monta um objeto em tempo de execução. Por isso ele fica de fora do type stripping do Node.js, que apaga os tipos e executa o arquivo .ts direto, sem build (estável desde o 24.12.0). O Node recusa sintaxe que não dá para simplesmente apagar, e enum está nessa lista, ao lado de namespace com código e constructor(private x).
O substituto usa um objeto as const e um tipo derivado com o mesmo nome:
// Substitui: enum StatusLead { Novo = "novo", ... }
export const StatusLead = {
Novo: "novo",
Qualificado: "qualificado",
Perdido: "perdido",
} as const;
// Mesmo nome para valor e tipo: o uso fica igual ao do enum
export type StatusLead = (typeof StatusLead)[keyof typeof StatusLead];
// ^?
export function rotulo(status: StatusLead): string {
switch (status) {
case StatusLead.Novo: return "Novo lead";
case StatusLead.Qualificado: return "Pronto para vendas";
case StatusLead.Perdido: return "Sem interesse";
}
}- linha 9
type StatusLead = "novo" | "qualificado" | "perdido" - resultado
sem erro de tipo, com strict
Você mantém StatusLead.Novo no código e ganha um bônus. A função aceita a string pura "qualificado" que chega de um webhook, enquanto um enum de string exigiria conversão. O JavaScript gerado é o próprio objeto, sem nada extra.
Para o compilador apontar qualquer enum que sobrar no projeto, ligue a opção que acusa sintaxe não apagável:
{
"compilerOptions": {
// acusa enum, namespace com código e parameter properties
"erasableSyntaxOnly": true
}
}Parâmetros de tipo const
Quando o array literal vai direto para uma função genérica (função com parâmetro de tipo, o T), quem chama precisaria lembrar do as const. O modificador const no parâmetro de tipo faz o compilador inferir como se a chamada tivesse as const:
function definirCanais<const T extends readonly string[]>(canais: T): T {
return canais;
}
const semConst = <T extends readonly string[]>(canais: T): T => canais;
const comConst = definirCanais(["email", "whatsapp"]);
// ^?
const comum = semConst(["email", "whatsapp"]);
// ^?- linha 7
const comConst: readonly ["email", "whatsapp"] - linha 9
const comum: string[] - resultado
sem erro de tipo, com strict
comConst sai como readonly ["email", "whatsapp"] e comum como string[]. Repare no readonly da restrição (o extends). Com uma restrição mutável como string[], a tupla somente leitura não cabe nela, e a inferência volta para o tipo largo. Esse recurso aparece bastante em funções que montam configuração de campanha, rotas de painel ou listas de campos de formulário, porque quem usa a função recebe os literais sem precisar digitar nada extra.
Tipos com intenção: valide entradas e limite as transições
Tipos com marca para valores que parecem iguais
Um email e um identificador de lead podem ser representados por string, mas têm usos diferentes. Branded types (tipos com uma marca exclusiva) permitem que o compilador diferencie esses valores. Na Mirante, isso evita passar o identificador de uma campanha onde uma integração espera o identificador de um lead.
A função de fábrica (função que verifica os dados e produz o valor tipado) concentra a criação desses tipos. Para representar falhas previstas, usaremos Result<T, E>: uma união de alternativas que carrega um valor T ou um erro E. Os parâmetros genéricos, escritos entre < >, permitem reutilizar o mesmo formato com tipos diferentes.
Nos exemplos de laboratório, os comentários erro esperado indicam diagnósticos intencionais do compilador. Remova essas linhas inválidas ao aproveitar o código no projeto.
type Result<T, E> =
| { ok: true; valor: T }
| { ok: false; erro: E };
declare const marca: unique symbol;
type Marca<T, Nome extends string> = T & { readonly [marca]: Nome };
type Email = Marca<string, "Email">;
type IdDoLead = Marca<string, "IdDoLead">;
type Centavos = Marca<number, "Centavos">;
function criarEmail(entrada: string): Result<Email, string> {
const valor = entrada.trim();
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor)
? { ok: true, valor: valor as Email }
: { ok: false, erro: "Formato de email inválido" };
}
function criarIdDoLead(entrada: string): Result<IdDoLead, string> {
return /^lead_[a-z0-9]+$/i.test(entrada)
? { ok: true, valor: entrada as IdDoLead }
: { ok: false, erro: "Identificador de lead inválido" };
}
function criarCentavos(entrada: number): Result<Centavos, string> {
return Number.isSafeInteger(entrada) && entrada >= 0
? { ok: true, valor: entrada as Centavos }
: { ok: false, erro: "Use um inteiro seguro e não negativo" };
}
const email = criarEmail("contato@mirante.ag");
// ^?
const identificador = criarIdDoLead("lead_a12");
const orcamento = criarCentavos(150000);
if (email.ok) console.log(email.valor);
// erro esperado: TS2322
const valorSemFabrica: Centavos = 150000;- linha 30
const email: Result<Email, string> - linha 37
TS2322: Type 'number' is not assignable to type 'Centavos'. Type 'number' is not assignable to type '{ readonly [marca]: "Centavos"; }'.
A marca usa unique symbol (um tipo de símbolo com identidade exclusiva), e & cria uma interseção, combinando o valor básico com essa marca. A propriedade readonly impede sua alteração pelo código tipado. Essas informações são apagadas na execução: o número continua sendo um número, sem uma propriedade extra criada pela fábrica.
A asserção de tipo (as, uma instrução para o compilador aceitar um tipo) fica depois da verificação. Espalhar as Email pelo projeto permitiria contornar essa regra. Em produção, mantenha as asserções dentro do módulo de criação e exporte as fábricas e os tipos necessários.
A expressão regular do exemplo verifica um formato simples de email; confirmar que a pessoa controla a caixa exige uma confirmação por mensagem. O padrão lead_... é uma convenção fictícia da Mirante. Para Centavos, a fábrica aceita somente inteiros seguros e não negativos, evitando valores fracionários e números fora da representação inteira segura.
Parse, don’t validate: converta na borda
Parse (interpretar uma entrada e produzir uma representação confiável) conecta dados externos aos tipos internos. A borda é o ponto de entrada da aplicação: um formulário, um webhook (requisição enviada por outro serviço) ou uma resposta de integração. Receba esses dados como unknown, tipo que exige verificação antes do uso.
Uma função que apenas responde true ou false deixa a conversão e o transporte dos dados a cargo de quem chamou. Um parser pode devolver o objeto pronto para uso, com campos normalizados e marcas aplicadas. No exemplo independente abaixo, a fábrica de email aparece novamente para manter o laboratório autocontido.
type Result<T, E> =
| { ok: true; valor: T }
| { ok: false; erro: E };
declare const marcaEmail: unique symbol;
type Email = string & { readonly [marcaEmail]: true };
type Lead = { readonly email: Email; readonly campanha: string };
type ErroLead = "estrutura" | "email";
function criarEmail(entrada: string): Result<Email, "email"> {
const valor = entrada.trim();
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor)
? { ok: true, valor: valor as Email }
: { ok: false, erro: "email" };
}
function lerLead(entrada: unknown): Result<Lead, ErroLead> {
if (typeof entrada !== "object" || entrada === null) {
return { ok: false, erro: "estrutura" };
}
if (!("email" in entrada) || typeof entrada.email !== "string" ||
!("campanha" in entrada) || typeof entrada.campanha !== "string" ||
entrada.campanha.trim() === "") {
return { ok: false, erro: "estrutura" };
}
const email = criarEmail(entrada.email);
if (!email.ok) return email;
return {
ok: true,
valor: { email: email.valor, campanha: entrada.campanha.trim() },
};
}
const entrada: unknown = { email: "contato@mirante.ag", campanha: "Captura" };
const recebido = lerLead(entrada);
// ^?
if (recebido.ok) console.log(recebido.valor.campanha);
// erro esperado: TS2322
const leadDireto: Lead = entrada;- linha 34
const recebido: Result<Lead, ErroLead> - linha 38
TS2322: Type 'unknown' is not assignable to type 'Lead'.
A propriedade ok funciona como discriminante (campo cujo valor identifica uma alternativa da união). Ao verificar recebido.ok, o TypeScript faz narrowing (estreitamento do tipo conforme uma condição): no ramo de sucesso, valor fica disponível. No ramo de falha, o código pode usar erro para escolher a mensagem do formulário.
Depois dessa conversão, passe Lead às funções internas, em vez de repassar o objeto externo. Isso reduz verificações repetidas e concentra a política de entrada em um lugar. Se os dados forem serializados e recebidos novamente por outra aplicação, essa nova borda precisa verificá-los outra vez.
Result para falhas que a interface precisa tratar
Result<T, E> torna explícitos os caminhos previstos, como email inválido, orçamento insuficiente ou lead duplicado. Cada função informa no retorno quais falhas fazem parte do seu contrato. A camada que atende o formulário pode transformar esses códigos em mensagens sem depender de try/catch (estrutura que captura exceções lançadas durante a execução).
Prefira códigos estáveis, como "email" e "estrutura", quando várias interfaces consumirem a mesma operação. A mensagem apresentada à pessoa pode variar conforme o contexto. Exceções continuam úteis para falhas inesperadas e invariantes violadas, isto é, condições internas que deveriam permanecer verdadeiras.
Há dois cuidados práticos ao compor operações:
- Verifique
okantes de acessar o valor. - Retorne a falha recebida quando ela já representar o problema.
- Converta falhas técnicas conhecidas em erros do domínio na camada responsável pela integração.
- Evite capturar qualquer exceção e classificá-la como “email inválido”, pois isso esconderia outros defeitos.
Máquina de estados para o botão de envio
Uma máquina de estados (modelo de situações possíveis e mudanças permitidas entre elas) ajuda a controlar o botão de captura. Na Mirante, o fluxo começa em pronto, passa por enviando e termina em sucesso ou falha. Somente a falha oferece uma nova tentativa.
O exemplo usa objetos com operações disponíveis por estado. Cada operação retorna o próximo estado com seu tipo específico. Assim, o código recebe sugestões apenas para as transições previstas e encontra um erro ao tentar reenviar pelo objeto de sucesso.
type Pronto = { readonly estado: "pronto"; enviar(): Enviando };
type Enviando = {
readonly estado: "enviando";
concluir(): Sucesso;
falhar(): Falha;
};
type Sucesso = { readonly estado: "sucesso" };
type Falha = { readonly estado: "falha"; tentarNovamente(): Enviando };
function criarBotao(): Pronto {
function iniciar(): Enviando {
return {
estado: "enviando",
concluir: () => ({ estado: "sucesso" }),
falhar: () => ({
estado: "falha",
tentarNovamente: iniciar,
}),
};
}
return { estado: "pronto", enviar: iniciar };
}
const pronto = criarBotao();
const enviando = pronto.enviar();
const sucesso = enviando.concluir();
// ^?
const falha = enviando.falhar();
const novaTentativa = falha.tentarNovamente();
// erro esperado: TS2339
sucesso.enviar();- linha 26
const sucesso: Sucesso - linha 32
TS2339: Property 'enviar' does not exist on type 'Sucesso'.
Esse modelo restringe as operações oferecidas por cada objeto, mas referências antigas continuam existindo. O exemplo consegue chamar concluir() e falhar() na mesma referência enviando; os tipos, sozinhos, não consomem essa referência. Na interface real, mantenha o estado atual em um único controlador e confira esse estado antes de executar efeitos, como enviar uma requisição.
Também desabilite o botão enquanto o envio estiver em andamento. Se duplicar a operação causar prejuízo, proteja a integração com idempotência (tratamento de repetições como a mesma operação). O servidor precisa dessa proteção mesmo quando a interface está corretamente tipada.
Builder em etapas para campanhas completas
Um builder em etapas (construtor que libera operações conforme os dados já fornecidos) representa a montagem gradual de um objeto. Ele ajuda quando a configuração possui uma sequência curta e relevante para o produto. Para a campanha da Mirante, primeiro escolhemos o público e depois informamos um orçamento já validado.
Cada etapa devolve um tipo com a próxima operação disponível. O objeto final só pode ser construído depois dos campos obrigatórios. O exemplo recebe Centavos como entrada confiável; no projeto, esse tipo deve vir do mesmo módulo da fábrica de orçamento.
declare const marcaCentavos: unique symbol;
type Centavos = number & { readonly [marcaCentavos]: true };
type Publico = "novos" | "ativos";
type Campanha = {
readonly publico: Publico;
readonly orcamento: Centavos;
};
type Inicio = { comPublico(publico: Publico): ComPublico };
type ComPublico = { comOrcamento(orcamento: Centavos): Completa };
type Completa = { construir(): Campanha };
function criarCampanha(): Inicio {
return {
comPublico: (publico) => ({
comOrcamento: (orcamento) => ({
construir: () => ({ publico, orcamento }),
}),
}),
};
}
function prepararCampanha(orcamento: Centavos): Campanha {
return criarCampanha()
.comPublico("novos")
.comOrcamento(orcamento)
.construir();
}
const inicio = criarCampanha();
// ^?
const comPublico = inicio.comPublico("ativos");
// erro esperado: TS2339
inicio.construir();- linha 29
const inicio: Inicio - linha 34
TS2339: Property 'construir' does not exist on type 'Inicio'.
As funções preservam os valores por closure (acesso de uma função às variáveis do contexto em que foi criada). Cada chamada cria a etapa seguinte sem alterar a anterior. Isso permite reaproveitar uma escolha de público para montar configurações diferentes.
Use esse padrão quando as etapas comunicarem uma regra real. Para um objeto pequeno, sem dependência entre campos, uma função que recebe todos os dados pode ser mais simples. Se houver muitas combinações e caminhos opcionais, avalie o custo de manter todos os tipos intermediários.
Exaustividade com assertNever reutilizável
Exaustividade é a garantia de que todas as alternativas de uma união foram tratadas. O tipo never representa um valor que não deveria existir naquele ponto do programa. Uma função assertNever recebe esse tipo e faz o compilador apontar qualquer alternativa esquecida.
O exemplo tem uma função completa e outra que esquece o estado de falha. Dentro de default, a versão incompleta ainda pode receber { estado: "falha" }, por isso a chamada gera o diagnóstico esperado. A exceção em assertNever também sinaliza uma violação caso dados incompatíveis alcancem esse caminho durante a execução.
type Estado =
| { estado: "pronto" }
| { estado: "enviando" }
| { estado: "sucesso" }
| { estado: "falha" };
function assertNever(valor: never): never {
throw new Error(`Estado inesperado: ${JSON.stringify(valor)}`);
}
function rotulo(estado: Estado): string {
switch (estado.estado) {
case "pronto": return "Enviar";
case "enviando": return "Enviando";
case "sucesso": return "Enviado";
case "falha": return "Tentar novamente";
default: return assertNever(estado);
}
}
function rotuloIncompleto(estado: Estado): string {
switch (estado.estado) {
case "pronto": return "Enviar";
case "enviando": return "Enviando";
case "sucesso": return "Enviado";
default:
// erro esperado: TS2345
return assertNever(estado);
}
}
const texto = rotulo({ estado: "pronto" });
// ^?- linha 32
const texto: string - linha 28
TS2345: Argument of type '{ estado: "falha"; }' is not assignable to parameter of type 'never'.
Mantenha assertNever em um módulo compartilhado e importe a mesma função nos pontos de decisão. Ao acrescentar um estado, os trechos exaustivos passam a exigir tratamento para ele. Evite um default que devolva um rótulo genérico, pois esse retorno aceitaria silenciosamente novas alternativas.
Para pattern matching (seleção de um resultado por padrões), ts-pattern 5.9 permite escrever a decisão em uma linha e verificar sua exaustividade com .exhaustive():
import { match } from "ts-pattern";
type Estado = "pronto" | "enviando" | "sucesso" | "falha";
function rotulo(estado: Estado): string {
return match(estado).with("pronto", () => "Enviar").with("enviando", () => "Enviando").with("sucesso", () => "Enviado").with("falha", () => "Tentar novamente").exhaustive();
}Classes: estado, contratos e recursos com prazo de uso
Campos e dois tipos de privacidade
Uma classe reúne estado (dados de uma instância) e métodos (funções ligadas a ela). Na Mirante, uma campanha pode guardar o orçamento em centavos e controlar suas próprias alterações. Os campos, ou propriedades declaradas na classe, deixam esses dados explícitos.
class Campanha {
readonly nome: string;
private revisoes = 0;
#orcamentoCentavos = 0;
constructor(nome: string) {
this.nome = nome;
}
definirOrcamento(centavos: number): void {
if (!Number.isInteger(centavos) || centavos < 0) {
throw new Error("Informe um valor inteiro e não negativo");
}
this.#orcamentoCentavos = centavos;
this.revisoes += 1;
}
resumo(): string {
return `${this.nome}: ${this.#orcamentoCentavos} centavos, revisão ${this.revisoes}`;
}
}
const campanha = new Campanha("Captação Mirante");
campanha.definirOrcamento(150_000);
console.log(campanha.resumo());- resultado
sem erro de tipo, com strict
O constructor é o método que inicializa cada instância; this aponta para a instância em uso. O modificador readonly impede reatribuir nome durante a verificação de tipos, depois da inicialização permitida. Ele também não torna objetos internos imutáveis, caso o campo guarde um objeto.
A escolha entre private e # muda onde a proteção acontece. A compilação, que transforma TypeScript em JavaScript, remove o modificador private. Já um campo privado nativo, escrito com #, mantém a proteção durante a execução.
| Recurso | private revisoes | #orcamentoCentavos |
|---|---|---|
| Verificação pelo TypeScript | Restringe acesso externo à classe | Restringe acesso externo à classe |
| Proteção durante a execução | O campo continua sendo uma propriedade comum | O campo tem privacidade aplicada pelo JavaScript |
| Acesso pela subclasse | Não é permitido diretamente | Não é permitido diretamente |
| Propriedade com nome em texto | Continua existindo como "revisoes" | Não equivale a uma propriedade chamada "#orcamentoCentavos" |
Use private quando a restrição durante o desenvolvimento atender ao projeto. Prefira # quando o próprio código em execução precisar preservar esse limite. Em ambos os casos, métodos públicos podem oferecer operações controladas sobre os dados.
Contratos com abstract e implements
Uma interface descreve um contrato: os membros que um objeto precisa oferecer. implements verifica se uma classe atende a esse contrato, sem copiar código nem preencher tipos de parâmetros automaticamente. Uma classe abstract, por sua vez, oferece uma base que precisa ser especializada antes de criar instâncias.
interface Publicador {
publicar(destino: string): string;
}
abstract class CampanhaBase {
abstract publicar(destino: string): string;
descricao(): string {
return "Campanha da Mirante";
}
}
class CampanhaEmail extends CampanhaBase implements Publicador {
override publicar(destino: string): string {
return `Envio preparado para ${destino}`;
}
override descricao(): string {
return `${super.descricao()} por e-mail`;
}
}
const campanha = new CampanhaEmail();
console.log(campanha.descricao());
console.log(campanha.publicar("lead@exemplo.com"));- resultado
sem erro de tipo, com strict
A palavra extends estabelece herança: a classe derivada recebe membros da classe base. O método abstrato publicar exige uma implementação concreta, enquanto descricao já oferece comportamento reutilizável. A chamada super.descricao() executa a implementação da base.
O modificador override declara que um membro substitui outro herdado. Se alguém remover ou renomear esse membro na base, o compilador aponta a inconsistência. A opção noImplicitOverride exige essa marcação nas sobrescritas de membros concretos; implementações de membros abstratos podem omiti-la, embora usá-la mantenha a intenção explícita.
{
"compilerOptions": {
"strict": true,
"target": "es2025",
"module": "esnext",
"noImplicitOverride": true,
"lib": ["es2025", "dom", "esnext.disposable"]
}
}O tsconfig.json reúne opções do compilador, e strict habilita um conjunto de verificações rigorosas. target define o nível de JavaScript emitido; module define o formato dos módulos, as unidades de código com importações e exportações. A opção lib seleciona declarações de APIs disponíveis para a checagem, incluindo as de descarte usadas adiante.
Decorators padrão para medir métodos
Um decorator é uma função que participa da definição de uma classe ou de seus membros, podendo acrescentar comportamento. Use o modelo padrão do TC39, o comitê que desenvolve o JavaScript, com experimentalDecorators desativado ou ausente. A opção experimental seleciona outro modelo, com assinaturas e regras diferentes.
O tipo ClassMethodDecoratorContext descreve o contexto de um método decorado, incluindo seu nome. Os genéricos, parâmetros que representam tipos, preservam a instância, os argumentos e o retorno do método original. Assim, a função adicionada pelo decorator mantém a assinatura que os consumidores já conhecem.
function medir<Instancia, Argumentos extends unknown[], Resultado>(
metodo: (this: Instancia, ...argumentos: Argumentos) => Resultado,
contexto: ClassMethodDecoratorContext<
Instancia,
(this: Instancia, ...argumentos: Argumentos) => Resultado
>,
) {
return function (
this: Instancia,
...argumentos: Argumentos
): Resultado {
const inicio = performance.now();
try {
return metodo.call(this, ...argumentos);
} finally {
const duracao = performance.now() - inicio;
console.log(`${String(contexto.name)}: ${duracao} ms`);
}
};
}
class PainelMetricas {
@medir
somarLeads(valores: number[]): number {
return valores.reduce((total, valor) => total + valor, 0);
}
}
const painel = new PainelMetricas();
console.log(painel.somarLeads([12, 18, 9]));- resultado
sem erro de tipo, com strict
O bloco finally executa a medição mesmo quando o método lança um erro. A chamada com .call(this, ...) preserva a instância original como contexto da execução. Esse decorator mede métodos síncronos: se o método retornar uma Promise, objeto que representa um resultado futuro, ele medirá apenas até esse objeto ser retornado.
O Node sem build, isto é, executando o arquivo TypeScript sem uma compilação prévia, ainda não executa decorators. Para esse exemplo, compile o arquivo antes de executá-lo no Node ou use uma ferramenta que transforme essa sintaxe. O apagamento de tipos feito pelo Node não realiza essa transformação.
accessor cria acesso intermediado ao campo
O modificador accessor declara um autoacessor: um par de operações de leitura e escrita com armazenamento interno gerado pelo compilador. Esse recurso permite trabalhar com a propriedade por meio de um getter e um setter, os métodos responsáveis por ler e atribuir valores. Decorators também podem atuar sobre autoacessores, com um contexto próprio para esse tipo de membro.
class Painel {
accessor titulo: string = "Leads da Mirante";
accessor atualizado: boolean = false;
}
const painel = new Painel();
painel.titulo = "Leads da campanha de novembro";
painel.atualizado = true;
console.log(painel.titulo);- resultado
sem erro de tipo, com strict
A sintaxe de consumo continua sendo painel.titulo, tanto na leitura quanto na atribuição. O armazenamento interno fica separado da propriedade pública de acesso. O accessor sozinho não valida valores: regras como rejeitar um título vazio precisam de implementação adicional.
using libera recursos ao sair do escopo
Um recurso descartável é um objeto que oferece uma operação padronizada de liberação. using registra essa operação para executá-la ao sair do escopo, o trecho delimitado em que a variável existe. O descarte também acontece quando há return ou uma exceção, o erro lançado durante a execução.
O protocolo síncrono usa Symbol.dispose, uma chave especial que identifica o método de liberação. A interface Disposable descreve esse contrato. No exemplo, a conexão é simulada para mostrar o momento do fechamento.
/// <reference lib="esnext.disposable" />
class Conexao implements Disposable {
#aberta = true;
consultarLeads(): number {
if (!this.#aberta) throw new Error("Conexão encerrada");
return 12;
}
[Symbol.dispose](): void {
this.#aberta = false;
console.log("Conexão liberada");
}
}
function contarLeads(): number {
using conexao = new Conexao();
return conexao.consultarLeads();
}
console.log(contarLeads());- resultado
sem erro de tipo, com strict
O método de descarte roda depois de calcular o retorno de contarLeads, antes de a função terminar. Quando há vários recursos no mesmo escopo, eles são liberados na ordem inversa à declaração. Evite devolver uma conexão declarada com using: ela já estará liberada quando quem chamou a função receber o objeto.
A referência a esnext.disposable fornece os tipos necessários para este bloco isolado. No projeto, a entrada equivalente já está no lib do tsconfig.json. Essas declarações não instalam implementações: o ambiente de execução também precisa oferecer os símbolos de descarte.
await using espera a liberação assíncrona
Quando o fechamento depende de uma operação assíncrona, use await using dentro de uma função async, capaz de aguardar resultados com await. O contrato AsyncDisposable usa Symbol.asyncDispose para identificar a liberação que pode precisar de espera. Esta segunda conexão também simula a integração, sem abrir uma conexão real.
/// <reference lib="esnext.disposable" />
class ConexaoRemota implements AsyncDisposable {
async consultarLeads(): Promise<number> {
return 24;
}
async [Symbol.asyncDispose](): Promise<void> {
await Promise.resolve();
console.log("Conexão remota liberada");
}
}
async function contarLeadsRemotos(): Promise<number> {
await using conexao = new ConexaoRemota();
return await conexao.consultarLeads();
}
async function atualizarPainel(): Promise<void> {
const total = await contarLeadsRemotos();
console.log(`Leads: ${total}`);
}
void atualizarPainel();- resultado
sem erro de tipo, com strict
O return await mantém a consulta dentro do escopo até ela terminar. Depois, a função aguarda o descarte antes de entregar seu resultado ao código que a chamou. Em uma integração real, esse método pode aguardar o fechamento da conexão ou a devolução dela ao gerenciador de conexões.
Importação adiada
Disponível desde o TypeScript 5.9, import defer * as integracao from "./integracao.js" declara uma importação adiada, que posterga a avaliação do módulo até o primeiro acesso às propriedades de seu namespace, o objeto que reúne suas exportações, e exige suporte no ambiente de execução.
Módulos: separe o código que roda dos tipos que descrevem
ESM e imports que existem só para o compilador
ESM (sistema de módulos do JavaScript baseado em import e export) organiza dependências entre arquivos. Na Mirante, um módulo pode concentrar o contrato dos leads e a função que prepara os dados do formulário. O contrato descreve a estrutura esperada; a função continua no JavaScript executado.
export interface Lead {
email: string;
campanhaId: string;
}
export function prepararLead(email: string, campanhaId: string): Lead {
return { email: email.trim(), campanhaId };
}- resultado
sem erro de tipo, com strict
import type importa apenas uma declaração usada na checagem de tipos. Para consumir esse arquivo, escreva import type { Lead } from "./lead.ts" e importe prepararLead separadamente como valor. Também existe a forma combinada: import { prepararLead, type Lead } from "./lead.ts".
export type faz o mesmo na saída do módulo: export type { Lead } from "./lead.ts" publica o tipo para outros arquivos, sem criar uma exportação no JavaScript. Isso ajuda em arquivos agregadores, que reúnem exportações de vários módulos. Classes exigem atenção: podem ser usadas como tipo e como valor, mas uma classe importada com import type fica indisponível para new.
verbatimModuleSyntax deixa a intenção explícita
verbatimModuleSyntax é a opção que preserva a sintaxe de módulos escrita no código, apagando os elementos marcados como tipos. Com ela, imports de valores permanecem no JavaScript, e imports exclusivos de tipos precisam dessa marcação. O compilador também sinaliza incompatibilidades entre a sintaxe ESM e arquivos tratados como CommonJS (sistema de módulos baseado em require).
Em um projeto ESM executado pelo Node.js, configure "type": "module" no package.json, o manifesto do pacote. Essa decisão precisa combinar com a configuração do compilador e com o ambiente que executará os arquivos. A extensão .mts também identifica explicitamente um módulo ESM, enquanto .cts identifica CommonJS.
Para o código da agência, uma convenção simples ajuda:
- Use
import typepara interfaces, aliases de tipo e contratos de propriedades. - Use imports comuns para funções, classes instanciadas e constantes.
- Use
import "./painel.css"quando a dependência precisa ser carregada pelos seus efeitos, como aplicar estilos. - Use
export typeao republicar contratos em um arquivo agregador.
nodenext, preserve e bundler ocupam lugares diferentes
module define como o TypeScript trata e emite módulos. moduleResolution define como ele procura o arquivo correspondente a cada import. Portanto, bundler é uma estratégia de resolução, enquanto nodenext e preserve são valores da opção module.
| Destino | module | moduleResolution | Consequência prática |
|---|---|---|---|
| Integração executada pelo Node.js | nodenext | nodenext | Segue as regras de módulos e pacotes do Node |
| Interface processada por um bundler | preserve | bundler | Preserva as formas de importação e aceita a resolução típica dessas ferramentas |
| Biblioteca publicada para terceiros | Conforme os formatos publicados | Conforme o ambiente atendido | Precisa conferir também as extensões e as declarações distribuídas |
Um bundler (ferramenta que resolve dependências e monta os arquivos da aplicação) pode aceitar imports que o Node rejeitaria diretamente. Por exemplo, moduleResolution: "bundler" aceita caminhos relativos sem extensão em situações nas quais o ESM do Node exige uma extensão. Escolha a configuração pelo executor real do projeto.
Para uma interface da Mirante cujo JavaScript será produzido por outra ferramenta, esta configuração mantém o TypeScript responsável pela checagem:
{
"compilerOptions": {
"strict": true,
"module": "preserve",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"]
}noEmit desativa a geração de arquivos pelo compilador. Essa combinação é adequada quando o bundler entende os imports usados pelo projeto. Ela não garante, sozinha, que esses mesmos arquivos possam ser executados diretamente no Node.
Extensões: escolha quem transforma os imports
No ESM do Node, caminhos relativos precisam de extensão. Quando o projeto gera JavaScript com tsc, uma opção tradicional é escrever import { prepararLead } from "./lead.js" no próprio arquivo TypeScript. O compilador consegue procurar o arquivo-fonte correspondente, mantendo no resultado o caminho .js.
Outra opção é escrever .ts nos imports e definir como eles serão tratados:
- Execução direta de TypeScript no Node: use
.tsnos caminhos relativos eimport typepara dependências exclusivas de tipos. - Checagem sem emissão:
allowImportingTsExtensionspermite caminhos terminados em.ts, comnoEmit. - Geração de JavaScript:
rewriteRelativeImportExtensionsreescreve extensões TypeScript em caminhos relativos para as extensões JavaScript correspondentes.
Para um serviço de captura que será compilado antes da execução, a configuração pode ser:
{
"compilerOptions": {
"strict": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"verbatimModuleSyntax": true,
"rewriteRelativeImportExtensions": true,
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}Nesse projeto, import { prepararLead } from "./lead.ts" vira um import de ./lead.js na saída. A reescrita cobre caminhos relativos com extensões reconhecidas; nomes de pacotes, aliases e caminhos calculados exigem tratamento próprio. Ela também não transforma automaticamente os destinos definidos no package.json.
Na execução direta, o Node faz type stripping (remoção dos tipos antes de executar). Ele não verifica tipos nem lê o tsconfig.json. Mantenha uma checagem separada com tsc --noEmit e use apenas sintaxe que esse executor consiga apagar.
Imports com #/ precisam de um resolvedor compatível
Subpath imports (nomes internos definidos no campo imports do package.json) permitem dar nomes estáveis a dependências do próprio pacote. Isso reduz sequências como ../../../ nos componentes do painel. O TypeScript 6.0 passou a aceitar caminhos iniciados por #/ na resolução bundler.
Em uma aplicação processada por bundler, o manifesto pode conter:
{
"name": "painel-mirante",
"private": true,
"type": "module",
"imports": {
"#/*": "./src/*.ts"
}
}Com esse mapa, import type { Lead } from "#/lead" aponta para src/lead.ts. Confirme se o bundler também interpreta esse campo e esse padrão. A aceitação pelo TypeScript cobre a checagem, enquanto a ferramenta de execução precisa resolver o caminho.
Para execução direta no Node, prefira nomes compatíveis com suas regras, como #lead, mapeado para o arquivo executado. O prefixo #/ não é aceito pelas regras de subpath imports do Node. Se houver compilação, confira se o destino do mapa aponta para o código de saída.
Arquivos .d.ts descrevem código existente
Um arquivo .d.ts é um arquivo de declarações: descreve tipos, valores e módulos sem fornecer sua implementação. Ele serve para documentar uma biblioteca JavaScript, uma API global ou recursos carregados por ferramentas externas. Esses arquivos também podem ser gerados pelo compilador para quem consome uma biblioteca TypeScript.
declare informa ao compilador que determinado elemento existe em outro lugar. A declaração precisa corresponder ao comportamento real da dependência. Se uma função declarada estiver ausente durante a execução, o arquivo de tipos não a criará.
Para uma biblioteca sem tipos, crie um módulo ambiente (declaração que descreve um módulo pelo nome importado):
declare module "captura-legada" {
export interface OpcoesCaptura {
chave: string;
}
export function iniciarCaptura(opcoes: OpcoesCaptura): void;
}O arquivo deve estar incluído no projeto, por exemplo pelo include do tsconfig.json. Depois disso, import { iniciarCaptura } from "captura-legada" passa a ter o contrato declarado. Evite declare module "captura-legada";, pois essa forma abre mão da descrição dos valores exportados.
declare global tipa o dataLayer
declare global amplia declarações disponíveis globalmente. Para tipar window.dataLayer, do Google Tag Manager, coloque essa ampliação dentro de um arquivo que seja um módulo. O export {} abaixo marca o arquivo como módulo sem publicar valores.
export {};
declare global {
interface EventoMirante {
event: string;
campanha_id?: string;
lead_id?: string;
}
interface Window {
dataLayer?: EventoMirante[];
}
}A propriedade opcional representa a possibilidade de o carregamento da página ainda não ter criado o array. Antes de enviar um evento, a aplicação pode usar window.dataLayer ??= [] e então chamar push. Essa declaração pressupõe as definições de tipos do navegador, disponíveis pela biblioteca DOM do TypeScript.
Esse contrato cobre eventos em formato de objeto usados pela Mirante. Se o projeto também envia outros formatos ao dataLayer, amplie o tipo para refletir esses usos. Evite declarar uma estrutura estreita que contradiga o código de rastreamento instalado.
CSS e SVG dependem do contrato do carregador
Imports de CSS e SVG dependem do carregador (parte da ferramenta que interpreta arquivos além de JavaScript). Uma declaração informa ao TypeScript o resultado esperado desse carregamento. Ela não instala suporte a esses formatos no Node ou no bundler.
Para CSS carregado apenas por efeito e SVG importado como URL, uma configuração possível é:
declare module "*.css" {
// Importado apenas para aplicar estilos.
}
declare module "*.svg" {
const url: string;
export default url;
}Com esse contrato, use import "./painel.css" e import logo from "./logo.svg". Se a ferramenta transforma SVG em componente de interface, a declaração precisa descrever esse componente. Para CSS Modules (estilos que exportam um mapa de nomes de classes), descreva esse mapa em uma declaração específica, como *.module.css.
Prefira as declarações fornecidas pelo próprio framework quando elas já correspondem ao projeto. Declarações genéricas podem esconder diferenças entre modos de importação. Com noUncheckedSideEffectImports, o compilador também verifica imports feitos apenas por efeito; o curinga de CSS reconhece o formato, mas não confirma a existência de cada arquivo.
Augmentation amplia um módulo já conhecido
Augmentation de módulo (ampliação dos tipos de um módulo existente) acrescenta membros a declarações que aceitam essa combinação, como interfaces. Diferentemente da declaração inicial de uma biblioteca sem tipos, essa ampliação ocorre em um arquivo que já é módulo. Um import no início estabelece esse contexto.
Para ampliar a interface OpcoesCaptura declarada anteriormente:
import "captura-legada";
declare module "captura-legada" {
interface OpcoesCaptura {
campanhaId?: string;
}
}Essa técnica só deve representar uma opção realmente aceita pela biblioteca ou por uma integração que a adapta. Ela altera a visão do compilador, sem modificar a implementação. Também não permite substituir livremente o tipo de uma propriedade existente ou criar novas exportações de topo no módulo ampliado.
isolatedDeclarations pede contratos explícitos na saída
isolatedDeclarations verifica se cada arquivo tem informação suficiente para gerar suas declarações sem analisar a implementação de outros arquivos. Isso ajuda ferramentas que produzem .d.ts separadamente. Em funções exportadas, adote retorno explícito; em valores exportados com estrutura inferida, declare também o tipo público.
export interface Orcamento {
campanhaId: string;
valorCentavos: number;
}
export function criarOrcamento(
campanhaId: string,
valorCentavos: number,
): Orcamento {
return { campanhaId, valorCentavos };
}
export const orcamentoInicial: Orcamento = {
campanhaId: "mirante-captura",
valorCentavos: 150000,
};- resultado
sem erro de tipo, com strict
Existem casos simples em que a opção aceita inferência, mas anotar os contratos exportados mantém a regra do projeto previsível. Funções internas podem continuar usando inferência normalmente. isolatedDeclarations faz a verificação dessas condições; a geração dos arquivos depende das opções de emissão.
Para um pacote de componentes ou integrações da Mirante que entrega somente declarações nesta etapa:
{
"compilerOptions": {
"strict": true,
"module": "preserve",
"moduleResolution": "bundler",
"declaration": true,
"emitDeclarationOnly": true,
"isolatedDeclarations": true,
"outDir": "dist/tipos"
},
"include": ["src"]
}declaration habilita os arquivos .d.ts, e emitDeclarationOnly limita a emissão a esses arquivos. Se essa configuração herdar outra com noEmit: true, sobrescreva a opção para false. O JavaScript e as declarações publicados precisam manter caminhos e exportações compatíveis para que os consumidores encontrem ambos.
tsconfig.json: seis cenários e catorze chaves de rigor
O tsconfig.json é o arquivo que diz ao compilador onde está o código, que JavaScript sai do outro lado e quão exigente ele deve ser. Depois do 6.0, boa parte do que se copiava de projeto em projeto virou padrão (strict, module esnext, target es2025) e outra parte foi descontinuada (baseUrl, moduleResolution node, target es5, outFile). Um arquivo bom em 2026 é curto e cada linha tem motivo.
A primeira decisão é quem transforma o TypeScript em JavaScript. Se é o Vite, o Next ou o Node com type stripping, o tsc só confere (noEmit) e a resolução de módulos imita a ferramenta (bundler para quem empacota, nodenext para o Node). Se é o próprio tsc ou o tsdown gerando uma biblioteca, entram declaration, isolatedDeclarations e rewriteRelativeImportExtensions. A oficina monta o arquivo por cenário e explica cada chave.
Seis arquivos prontos, comentados linha a linha
Aplicação Vite + React
{
"compilerOptions": {
"target": "ES2023", // Compila para sintaxe moderna suportada pelos navegadores atuais
"lib": ["ES2023", "DOM", "DOM.Iterable"], // Inclui APIs do JS moderno e manipulação do DOM no navegador
"module": "preserve", // Mantém a sintaxe original de import/export para o Vite otimizar
"moduleResolution": "bundler", // Resolve módulos como empacotadores modernos (suporta exports do package.json sem exigir extensão)
"jsx": "react-jsx", // Emite JSX usando o runtime automático do React 17+ (sem precisar importar React)
"allowImportingTsExtensions": true, // Permite importar arquivos com extensão explícita .ts/.tsx
"verbatimModuleSyntax": true, // Força o uso de 'import type' para tipos, garantindo remoção limpa pelo bundler
"isolatedModules": true, // Garante que cada arquivo possa ser transpilado isoladamente pelo esbuild/SWC
"noEmit": true, // O TypeScript apenas checa tipos; o Vite faz o build final
"strict": true, // Ativa toda a família de checagens estritas de tipos
"noUncheckedIndexedAccess": true, // Acessos por índice em arrays/objetos retornam 'T | undefined'
"exactOptionalPropertyTypes": true, // Impede atribuir 'undefined' explicitamente a propriedades opcionais sem '| undefined'
"skipLibCheck": true // Pula a checagem de arquivos .d.ts em node_modules para acelerar o processo
},
"include": ["src"] // Define a pasta de código-fonte a ser analisada
}Aplicação Next.js (App Router)
{
"compilerOptions": {
"target": "ES2023", // Define o nível de sintaxe JS para o ambiente híbrido (Node/Edge/Browser)
"lib": ["DOM", "DOM.Iterable", "ES2023"], // Declarações de ambiente para componentes Client e Server
"module": "preserve", // Preserva imports dinâmicos e estáticos para o Turbopack/Webpack processar
"moduleResolution": "bundler", // Alinha a resolução de módulos com o comportamento interno do Next.js
"jsx": "preserve", // Mantém o JSX intacto para o compilador SWC do Next.js transformar
"verbatimModuleSyntax": true, // Evita imports acidentais de módulos de servidor em componentes de cliente quando são apenas tipos
"isolatedModules": true, // Obrigatório para compatibilidade com o compilador SWC do Next.js
"noEmit": true, // Desativa a geração de arquivos pelo tsc; o Next.js gerencia o build
"incremental": true, // Salva cache de checagem de tipos em disco para acelerar execuções futuras
"strict": true, // Habilita o modo estrito completo do TypeScript
"noUncheckedIndexedAccess": true, // Protege contra erros de runtime ao ler parâmetros de rotas ou arrays vazios
"skipLibCheck": true, // Ignora conflitos de tipos internos entre bibliotecas de terceiros
"paths": {
"@/*": ["./src/*"] // Configura atalho de importação para a raiz do código-fonte
},
"plugins": [
{ "name": "next" } // Ativa o plugin de linguagem do Next.js (valida Server Actions, Page props e rotas)
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], // Inclui tipos gerados automaticamente pelas rotas do Next.js
"exclude": ["node_modules"] // Ignora dependências externas
}API Node.js com Type Stripping nativo (sem build)
{
"compilerOptions": {
"target": "ES2024", // Alinha com a versão do V8 presente no Node.js 22.6+ / 24 LTS
"lib": ["ES2024"], // Inclui apenas APIs nativas do ECMAScript (sem DOM)
"module": "nodenext", // Impõe regras estritas de ESM/CJS nativos do Node.js
"moduleResolution": "nodenext", // Exige extensões explícitas em imports relativos e respeita subpath exports
"allowImportingTsExtensions": true, // Permite 'import { x } from "./file.ts"', exigido pelo Node nativo sem build
"erasableSyntaxOnly": true, // Bloqueia sintaxes que geram código JS (enums, namespaces, parameter properties), compatibilizando com o type stripping do Node
"verbatimModuleSyntax": true, // Obrigatório no Node nativo: garante que imports de tipos tenham a flag 'type' para não quebrarem em runtime
"noEmit": true, // Nenhum arquivo .js é gerado; o Node executa os arquivos .ts diretamente
"strict": true, // Ativa validação estrita de tipos
"noUncheckedIndexedAccess": true, // Força validação de existência ao acessar arrays e dicionários
"exactOptionalPropertyTypes": true, // Diferencia chave ausente de chave com valor 'undefined' (crucial para payloads JSON/DB)
"skipLibCheck": true // Acelera o tsc ignorando checagem profunda de node_modules
},
"include": ["src"] // Analisa apenas o diretório da aplicação
}Biblioteca publicada no npm (isolatedDeclarations e tsdown)
{
"compilerOptions": {
"target": "ES2023", // Gera código moderno compatível com runtimes vigentes
"lib": ["ES2023"], // Limita o escopo às APIs padrão da linguagem para máxima portabilidade
"module": "nodenext", // Garante compatibilidade estrita com o sistema de módulos do ecossistema npm
"moduleResolution": "nodenext", // Força verificação rigorosa de imports para não quebrar consumidores Node ESM
"rewriteRelativeImportExtensions": true, // Reescreve imports '.ts' para '.js' automaticamente caso o tsc seja usado para emitir
"declaration": true, // Habilita a geração de arquivos .d.ts para os consumidores da biblioteca
"declarationMap": true, // Gera sourcemaps para os .d.ts (permite "Go to Definition" ir direto ao código-fonte)
"isolatedDeclarations": true, // Exige tipos explícitos nas funções/variáveis exportadas, permitindo que o tsdown/Rolldown gere .d.ts em paralelo sem o tsc
"erasableSyntaxOnly": true, // Garante que a biblioteca contenha apenas tipos apagáveis, facilitando o tree-shaking
"verbatimModuleSyntax": true, // Previne efeitos colaterais de imports elididos incorretamente no pacote final
"strict": true, // Garante que os tipos exportados sejam precisos e seguros
"noUncheckedIndexedAccess": true, // Evita vazar tipos inseguros de indexação para a API pública
"exactOptionalPropertyTypes": true, // Torna os contratos de opções da biblioteca estritos e previsíveis
"skipLibCheck": true, // Foca a checagem apenas no código da própria biblioteca
"outDir": "./dist", // Diretório de saída padrão
"rootDir": "./src" // Diretório raiz do código-fonte
},
"include": ["src"] // Processa apenas os arquivos de código da biblioteca
}Monorepo com Project References (Configuração do Pacote/Projeto)
{
"compilerOptions": {
"target": "ES2023", // Padroniza a saída JS entre os pacotes do monorepo
"lib": ["ES2023"], // Define a biblioteca base compartilhada
"module": "nodenext", // Padrão de módulos para pacotes internos se comunicarem
"moduleResolution": "nodenext", // Resolve pacotes internos via 'exports' do package.json
"rewriteRelativeImportExtensions": true, // Converte extensões .ts locais para .js no build incremental
"composite": true, // Obrigatório para Project References: permite que o tsc rastreie dependências entre pacotes
"declaration": true, // Obrigatório para 'composite': gera os contratos .d.ts consumidos por outros pacotes do monorepo
"declarationMap": true, // Permite navegar entre pacotes do monorepo no editor sem precisar ir para a pasta dist
"isolatedDeclarations": true, // Acelera drasticamente builds em monorepos permitindo geração paralela de .d.ts
"verbatimModuleSyntax": true, // Mantém consistência de imports entre ferramentas de build distintas no monorepo
"strict": true, // Padroniza o rigor de tipos em todos os pacotes
"noUncheckedIndexedAccess": true, // Segurança de acesso a coleções em todo o workspace
"skipLibCheck": true, // Evita revalidar tipos externos em cada subpacote
"outDir": "./dist", // Pasta de saída do pacote atual
"rootDir": "./src" // Pasta de entrada do pacote atual
},
"include": ["src"], // Escopo do pacote atual
"references": [
{ "path": "../core" }, // Declara dependência explícita do pacote 'core' para ordenação do grafo de build
{ "path": "../shared-utils" } // Declara dependência explícita do pacote 'shared-utils'
]
}Script Bun ou Deno
{
"compilerOptions": {
"target": "ESNext", // Utiliza os recursos mais recentes suportados pelos runtimes nativos modernos
"lib": ["ESNext", "DOM", "DOM.Iterable"], // Inclui APIs Web padrão (fetch, WebSocket, Request/Response) nativas no Bun/Deno
"module": "preserve", // Deixa o runtime resolver módulos nativamente (suporta npm:, jsr:, https:)
"moduleResolution": "bundler", // Permite resolução flexível e imports diretos sem burocracia de CJS/ESM legado
"allowImportingTsExtensions": true, // Padrão no Deno e Bun para importar arquivos .ts diretamente
"verbatimModuleSyntax": true, // Garante separação explícita entre imports de valor e imports de tipo
"noEmit": true, // Bun e Deno executam TypeScript nativamente sem etapa de build
"strict": true, // Habilita checagem estrita
"noUncheckedIndexedAccess": true, // Evita falhas de índice fora dos limites em scripts de automação
"skipLibCheck": true // Acelera a verificação no LSP do editor
},
"include": ["**/*.ts"] // Verifica todos os scripts no diretório
}Catorze chaves de rigor e o custo de ligar cada uma
Projeto novo liga todas. Projeto antigo liga uma por vez, começando pelas de custo baixo, e usa a contagem de erros no CI para não deixar o número subir. strictNullChecks e noImplicitAny já vêm dentro de strict; as outras são extras.
| Chave | O que pega | Custo num projeto existente |
|---|
DOM tipado: valide campos, respostas e eventos
Selecione o elemento e trate a ausência
O DOM (modelo de objetos da página) permite acessar formulários, botões e outros elementos pelo JavaScript. No TypeScript, querySelector<T> recebe um genérico (parâmetro de tipo) para indicar o elemento esperado. O resultado continua incluindo null, porque o seletor pode encontrar uma página sem aquele elemento.
O genérico informa uma expectativa ao compilador; ele não verifica a classe real do elemento durante a execução. Use seletores específicos e confira o HTML da página. Quando a origem do elemento for incerta, use também instanceof (checagem da classe do objeto em execução).
Os exemplos abaixo são independentes. Neste primeiro, o formulário da Mirante tem id="captura" e contém um campo de e-mail com name="email".
const formulario = document.querySelector<HTMLFormElement>("form#captura");
// ^?
const email = document.querySelector<HTMLInputElement>('input[name="email"]');
if (formulario === null || email === null) {
throw new Error("Formulário de captura incompleto");
}
formulario.addEventListener("submit", (evento) => {
const submissao: SubmitEvent = evento;
const alvo = submissao.currentTarget;
if (!(alvo instanceof HTMLFormElement)) return;
submissao.preventDefault();
console.log(new FormData(alvo), submissao.submitter);
});
email.addEventListener("input", (evento) => {
if (!(evento instanceof InputEvent)) return;
const alvo = evento.currentTarget;
if (!(alvo instanceof HTMLInputElement)) return;
console.log(alvo.value, evento.inputType);
});
email.addEventListener("keydown", (evento) => {
const tecla: KeyboardEvent = evento;
const alvo = tecla.currentTarget;
if (!(alvo instanceof HTMLInputElement)) return;
if (tecla.key === "Escape") alvo.value = "";
});- linha 1
const formulario: HTMLFormElement | null - resultado
sem erro de tipo, com strict
addEventListener registra uma função para receber eventos (notificações de ações na página). SubmitEvent descreve o envio do formulário e expõe submitter, o elemento que iniciou o envio, que também pode ser null. InputEvent traz detalhes de edição, como inputType, enquanto KeyboardEvent fornece informações do teclado, como key.
currentTarget identifica o elemento cujo ouvinte está sendo executado; target aponta para a origem do evento, que pode ser um descendente. Como currentTarget tem tipo amplo e pode ser null, o exemplo verifica sua classe antes de acessar value ou criar o FormData. Essa verificação faz o narrowing (refinamento de um tipo após uma condição).
O evento "input" pode chegar como um Event genérico, especialmente quando criado por outro código. Por isso, o exemplo verifica InputEvent antes de acessar informações específicas da edição. Se a tarefa exigir apenas ler o valor atual, a checagem de currentTarget como HTMLInputElement já atende.
Transforme FormData em um objeto validado
FormData coleta os valores enviados pelos controles do formulário. Cada leitura pode retornar texto, um File (objeto que representa um arquivo) ou null; campos com o mesmo nome também podem ter vários valores. Para nome, e-mail e orçamento, o contrato (formato de dados combinado) da Mirante aceita um único texto por campo.
A coerção (conversão de um valor para outra representação) transforma o orçamento textual em centavos inteiros. A expressão regular (padrão de correspondência de texto) abaixo aceita 1250, 1250,90 e 1250.90, sem separadores de milhar ou símbolo monetário. Os erros ficam associados aos nomes dos campos.
type LeadNovo = { nome: string; email: string; orcamentoCentavos: number };
type Erros = { nome?: string; email?: string; orcamento?: string };
type Resultado =
| { ok: true; lead: LeadNovo }
| { ok: false; erros: Erros };
function lerFormulario(formulario: HTMLFormElement): Resultado {
const dados = new FormData(formulario);
const texto = (campo: string): string => {
const valor = dados.get(campo);
return typeof valor === "string" ? valor.trim() : "";
};
const nome = texto("nome");
const email = texto("email");
const bruto = texto("orcamento");
const erros: Erros = {};
if (nome.length < 2) erros.nome = "Informe pelo menos dois caracteres";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
erros.email = "Confira o e-mail";
}
const partes = /^(\d+)(?:[,.](\d{1,2}))?$/.exec(bruto);
let orcamentoCentavos = 0;
if (partes === null) {
erros.orcamento = "Use um valor como 1250,90";
} else {
const reais = Number(partes[1]);
const centavos = Number((partes[2] ?? "").padEnd(2, "0"));
orcamentoCentavos = reais * 100 + centavos;
if (!Number.isSafeInteger(orcamentoCentavos) || orcamentoCentavos <= 0) {
erros.orcamento = "Informe um orçamento positivo dentro do limite";
}
}
if (Object.keys(erros).length > 0) return { ok: false, erros };
return { ok: true, lead: { nome, email, orcamentoCentavos } };
}
const formulario = document.querySelector<HTMLFormElement>("form#captura");
if (formulario !== null) {
console.log(lerFormulario(formulario));
}- resultado
sem erro de tipo, com strict
Resultado é uma união discriminada (alternativas de tipo identificadas por uma propriedade comum). Ao testar resultado.ok, o compilador libera resultado.lead no ramo de sucesso e resultado.erros no ramo de falha. Isso permite enviar somente os dados aprovados e apresentar cada mensagem junto ao respectivo campo.
A conversão separa reais e centavos antes de produzir o inteiro, evitando depender de multiplicação direta de um decimal monetário. Number.isSafeInteger verifica se o resultado é um inteiro representável com segurança. A checagem de e-mail é básica e deve acompanhar a validação no servidor.
Para apresentar as mensagens, associe cada campo ao elemento de erro com aria-describedby (atributo de acessibilidade que referencia uma descrição). Marque os campos inválidos com aria-invalid="true" e escreva mensagens usando textContent (propriedade que insere texto sem interpretá-lo como HTML). Caso um campo aceite várias escolhas, leia seus valores com getAll e valide a coleção.
Receba a resposta de rede como unknown
fetch inicia uma requisição de rede e retorna uma Promise (resultado disponível de forma assíncrona). Uma resposta HTTP (protocolo de comunicação entre cliente e servidor) de erro ainda pode resolver essa promessa normalmente. Confira resposta.ok antes de tratar o conteúdo como sucesso.
O JSON (formato textual de dados) recebido deve entrar no código como unknown, tipo que exige verificação antes do uso. Um predicado de tipo (função cujo retorno informa um refinamento) valida o contrato esperado. Escrever apenas uma asserção como as LeadCriado dispensaria essa verificação em execução.
O exemplo inclui AbortController (controlador que pode cancelar uma operação) e um timeout (prazo máximo de espera). A união de retorno separa erro HTTP, prazo excedido, falha de rede e resposta incompatível.
type LeadCriado = { id: string };
type ResultadoRede =
| { ok: true; lead: LeadCriado }
| { ok: false; erro: "http"; status: number }
| { ok: false; erro: "timeout" | "rede" | "resposta" };
function leadValido(dado: unknown): dado is LeadCriado {
return typeof dado === "object" && dado !== null &&
"id" in dado && typeof dado.id === "string" && dado.id.trim().length > 0;
}
async function enviarLead(
dados: { nome: string; email: string; orcamentoCentavos: number },
): Promise<ResultadoRede> {
const controle = new AbortController();
let expirou = false;
let lendoResposta = false;
const temporizador = setTimeout(() => {
expirou = true;
controle.abort();
}, 8000);
try {
const resposta = await fetch("/api/leads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(dados),
signal: controle.signal,
});
if (!resposta.ok) return { ok: false, erro: "http", status: resposta.status };
lendoResposta = true;
const dado: unknown = await resposta.json();
if (!leadValido(dado)) return { ok: false, erro: "resposta" };
return { ok: true, lead: { id: dado.id } };
} catch {
return { ok: false, erro: expirou ? "timeout" : lendoResposta ? "resposta" : "rede" };
} finally {
clearTimeout(temporizador);
}
}- resultado
sem erro de tipo, com strict
O prazo cobre tanto a espera pela resposta quanto a leitura do corpo. O bloco finally executa a limpeza ao concluir a operação, inclusive quando há retorno antecipado. Neste contrato, a categoria "resposta" inclui falhas ao ler ou interpretar o corpo e dados que não passaram na validação.
Na interface, use erro para decidir a mensagem e consulte status somente no ramo "http". Um cancelamento local também pode acontecer depois que o servidor recebeu o pedido. Para evitar cadastros duplicados, combine o bloqueio temporário do botão de envio com uma estratégia de deduplicação no servidor.
Use dataset como configuração textual
dataset expõe atributos data-* como propriedades de texto. Por exemplo, data-campanha="captura" fica disponível em elemento.dataset.campanha, e data-orcamento-centavos vira dataset.orcamentoCentavos. Uma propriedade ausente retorna undefined.
Esses valores precisam de validação quando representam números, permissões ou opções restritas. Um atributo com texto "false" continua sendo uma string preenchida. Para um estado booleano textual, compare explicitamente com "true" em vez de usar Boolean(valor).
Crie um botão com estado e evento próprio
Um web component (componente baseado em recursos nativos do navegador) pode encapsular estado e comportamento. O exemplo usa um custom element (elemento HTML registrado pela aplicação) chamado mirante-botao. Para colocá-lo na página, use <mirante-botao data-campanha="captura"></mirante-botao>.
O componente mantém um estado de seleção e usa um botão HTML nativo internamente. O Shadow DOM (árvore de elementos encapsulada no componente) abriga esse botão. aria-pressed comunica seu estado de pressionamento às tecnologias assistivas.
type DetalheBotao = { campanha: string; ativo: boolean };
class BotaoCampanha extends HTMLElement {
private ativo = false;
private readonly botao = document.createElement("button");
constructor() {
super();
this.botao.type = "button";
this.attachShadow({ mode: "open" }).append(this.botao);
this.botao.addEventListener("click", () => {
this.ativo = !this.ativo;
this.atualizar();
this.dispatchEvent(new CustomEvent<DetalheBotao>("mirante:estado", {
detail: {
campanha: this.dataset.campanha ?? "geral",
ativo: this.ativo,
},
bubbles: true,
composed: true,
}));
});
}
connectedCallback(): void {
this.atualizar();
}
private atualizar(): void {
this.botao.textContent = this.ativo ? "Campanha selecionada" : "Selecionar campanha";
this.botao.setAttribute("aria-pressed", String(this.ativo));
this.dataset.ativo = String(this.ativo);
}
}
customElements.define("mirante-botao", BotaoCampanha);- resultado
sem erro de tipo, com strict
connectedCallback é um método de ciclo de vida (ponto chamado pelo navegador durante a existência do componente), executado quando o elemento se conecta ao documento. O ouvinte do botão é registrado no construtor para evitar registros repetidos em reconexões. O atributo data-ativo espelha o estado para consulta externa; neste exemplo, alterar esse atributo externamente não modifica o estado interno.
CustomEvent<DetalheBotao> descreve o conteúdo de detail, a propriedade que transporta os dados do evento. bubbles permite que o evento se propague aos ancestrais; composed permite atravessar limites de Shadow DOM. Aqui, o evento já parte do próprio componente, e essa configuração também permite sua propagação quando ele estiver dentro de outro componente encapsulado.
Valide o detalhe ao receber eventos externos
O genérico de CustomEvent verifica o código que constrói o evento. Outro código na página ainda pode emitir um evento com o mesmo nome e conteúdo diferente. No receptor, trate o detalhe como unknown e confira os campos antes de atualizar o painel de métricas.
type DetalheBotao = { campanha: string; ativo: boolean };
function detalheValido(dado: unknown): dado is DetalheBotao {
return typeof dado === "object" && dado !== null &&
"campanha" in dado && typeof dado.campanha === "string" &&
"ativo" in dado && typeof dado.ativo === "boolean";
}
document.addEventListener("mirante:estado", (evento) => {
if (!(evento instanceof CustomEvent)) return;
const detalhe: unknown = evento.detail;
if (!detalheValido(detalhe)) return;
const painel = document.querySelector<HTMLOutputElement>("output#estado-campanha");
if (painel === null) return;
painel.textContent = detalhe.ativo
? `Campanha ${detalhe.campanha} selecionada`
: `Campanha ${detalhe.campanha} desmarcada`;
});- resultado
sem erro de tipo, com strict
O receptor depende apenas do nome do evento e do formato validado do detalhe. Ele pode atualizar um elemento <output id="estado-campanha"></output> sem acessar o botão interno ou alterar diretamente seu estado. Mantenha esse contrato pequeno para facilitar a integração entre formulário, campanha e painel.
Valide o lead antes de usar seus campos
O tipo desaparece, o dado continua chegando
Os tipos do TypeScript são apagados na compilação para JavaScript. Uma declaração como type Lead orienta o compilador, mas deixa a validação em tempo de execução (checagem enquanto o programa roda) por sua conta. Dados de formulário, webhook (notificação enviada por outro sistema), API (interface de comunicação entre sistemas) e localStorage (armazenamento local do navegador) precisam passar por essa checagem.
Receba essas entradas como unknown, o tipo que exige verificar um valor antes de acessar seus campos. Algumas APIs retornam tipos permissivos; explicitar unknown na fronteira mantém essa exigência no seu código. Uma asserção como as Lead apenas instrui o compilador a confiar no valor.
function lerJson(texto: string): unknown {
return JSON.parse(texto);
}
const entrada = lerJson('{"nome":"Ana","orcamento":"5000"}');
if (typeof entrada === "object" && entrada !== null) {
console.log("Objeto recebido; os campos ainda precisam de validação");
}- resultado
sem erro de tipo, com strict
O JSON (formato textual de dados) desse exemplo é válido, mas orcamento contém uma string (texto). JSON.parse verifica a sintaxe do documento e pode lançar uma exceção se ela estiver incorreta. A próxima etapa verifica os campos, seus tipos e as regras da Mirante.
Um contrato de lead para todas as bibliotecas
Um esquema é uma descrição executável dos dados aceitos. Os cinco exemplos abaixo representam as mesmas regras de negócio e recebem um objeto já montado. O orçamento chega como número em centavos, sem conversão automática.
nome: texto com pelo menos dois caracteres.email: endereço com sintaxe de email.whatsapp: opcional; quando presente, deve corresponder à expressão regular (padrão de texto)/^\+[1-9]\d{7,14}$/.orcamento: número inteiro em centavos.canal: um dos valoressite,anuncioouindicacao.aceite: exatamentetrue.
A expressão do WhatsApp exige +, um primeiro dígito diferente de zero e mais sete a quatorze dígitos. Ela confere o formato; a existência do número exige outra verificação. O mesmo cuidado vale para email: verificar sintaxe e confirmar que a pessoa controla a caixa postal são etapas distintas.
Um campo opcional pode estar ausente. A string vazia continua sendo um valor presente e falha na expressão do WhatsApp. Na montagem do objeto do formulário, remova esse campo quando estiver vazio e converta o orçamento conforme a unidade exibida na interface.
Zod 4: objeto, inferência e resultado explícito
O Zod usa uma API encadeável (métodos chamados em sequência) para compor regras. z.object descreve o objeto, e z.infer extrai seu tipo TypeScript, evitando manter uma segunda definição manual. safeParse retorna uma união discriminada (alternativas identificadas por um campo), com success indicando o caminho válido.
import { z } from "zod";
export const esquemaLead = z.object({
nome: z.string().min(2),
email: z.email(),
whatsapp: z.string().regex(/^\+[1-9]\d{7,14}$/).optional(),
orcamento: z.number().int(),
canal: z.enum(["site", "anuncio", "indicacao"]),
aceite: z.literal(true),
});
export type Lead = z.infer<typeof esquemaLead>;
export function lerLead(entrada: unknown): Lead {
const resultado = esquemaLead.safeParse(entrada);
if (!resultado.success) {
throw new Error(
resultado.error.issues.map((falha) => falha.message).join("; "),
);
}
return resultado.data;
}Depois da verificação de success, resultado.data possui o tipo inferido do esquema. A chamada a safeParse representa falhas de validação no resultado; o exemplo escolhe transformá-las em exceção dentro de lerLead. Em um formulário, preserve a lista de problemas para apresentar cada mensagem junto ao campo correspondente.
Zod Mini: composição com funções
O Zod Mini oferece uma API funcional (funções que recebem e devolvem esquemas). Esse desenho favorece tree shaking, a remoção de código não utilizado durante o empacotamento. As regras continuam próximas das do Zod completo, com verificações adicionais aplicadas por check.
import * as z from "zod/mini";
export const esquemaLead = z.object({
nome: z.string().check(z.minLength(2)),
email: z.email(),
whatsapp: z.optional(
z.string().check(z.regex(/^\+[1-9]\d{7,14}$/)),
),
orcamento: z.int(),
canal: z.enum(["site", "anuncio", "indicacao"]),
aceite: z.literal(true),
});
export type Lead = z.infer<typeof esquemaLead>;
export function validarLead(entrada: unknown) {
return z.safeParse(esquemaLead, entrada);
}Aqui, z.optional envolve o esquema do WhatsApp, enquanto z.safeParse recebe o esquema como primeiro argumento. O retorno também usa success, data e informações de erro. Compare o tamanho do arquivo JavaScript produzido pelo projeto para decidir se a mudança de estilo compensa.
Valibot 1.5: regras em sequência com pipe
No Valibot, v.pipe organiza uma sequência de verificações. A primeira etapa estabelece o tipo básico; as seguintes acrescentam restrições e mensagens. v.InferOutput extrai o tipo de saída, inclusive quando um esquema inclui transformações (alterações do valor durante o processamento).
import * as v from "valibot";
export const esquemaLead = v.object({
nome: v.pipe(v.string(), v.minLength(2)),
email: v.pipe(v.string(), v.email()),
whatsapp: v.optional(
v.pipe(v.string(), v.regex(/^\+[1-9]\d{7,14}$/)),
),
orcamento: v.pipe(v.number(), v.integer()),
canal: v.picklist(["site", "anuncio", "indicacao"]),
aceite: v.literal(true),
});
export type Lead = v.InferOutput<typeof esquemaLead>;
export function validarLead(entrada: unknown) {
const resultado = v.safeParse(esquemaLead, entrada);
if (resultado.success) {
console.log(resultado.output.nome);
}
return resultado;
}O valor validado fica em output, em vez de data. A composição modular favorece projetos que precisam controlar o código enviado ao navegador. Para mensagens de formulário, ações como v.minLength(2, "Informe pelo menos dois caracteres") permitem escrever o texto perto da regra.
ArkType 2.2: regras próximas da sintaxe de tipos
O ArkType usa uma DSL (linguagem específica para descrever um domínio) em strings. "string >= 2" restringe o comprimento do texto, e "string.email" aplica a validação de email. O ponto de interrogação na chave "whatsapp?" torna a propriedade opcional.
import { type } from "arktype";
export const esquemaLead = type({
nome: "string >= 2",
email: "string.email",
"whatsapp?": /^\+[1-9]\d{7,14}$/,
orcamento: "number.integer",
canal: "'site' | 'anuncio' | 'indicacao'",
aceite: "true",
});
export type Lead = typeof esquemaLead.infer;
export function validarLead(entrada: unknown) {
const resultado = esquemaLead(entrada);
if (resultado instanceof type.errors) {
console.error(resultado.summary);
}
return resultado;
}O esquema funciona como uma função: recebe o dado e devolve o resultado validado ou uma coleção de erros. A verificação com instanceof type.errors separa essas alternativas. Essa sintaxe pode agradar equipes que preferem ler as restrições junto da estrutura do objeto.
Effect Schema: composição e integração com Effect
O módulo effect/Schema também descreve estruturas e verificações. No Effect 4, check acrescenta restrições ao esquema, enquanto Schema.Struct organiza os campos. O exemplo usa uma expressão regular para a sintaxe de email e prepara a integração compartilhada que aparece a seguir.
import { Schema } from "effect";
export const esquemaLead = Schema.Struct({
nome: Schema.String.check(Schema.isMinLength(2)),
email: Schema.String.check(
Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
),
whatsapp: Schema.optional(
Schema.String.check(Schema.isPattern(/^\+[1-9]\d{7,14}$/)),
),
orcamento: Schema.Number.check(Schema.isInt()),
canal: Schema.Literals(["site", "anuncio", "indicacao"]),
aceite: Schema.Literal(true),
});
export type Lead = typeof esquemaLead.Type;
export const esquemaPadrao = Schema.toStandardSchemaV1(esquemaLead);
export function validarLead(entrada: unknown) {
return esquemaPadrao["~standard"].validate(entrada);
}As regras de negócio são as mesmas, mas os validadores de email das bibliotecas podem divergir em casos-limite. A expressão usada no exemplo do Effect é uma checagem simplificada, sem pretensão de cobrir todos os endereços possíveis. Se uma integração exige aceitação idêntica entre implementações, compartilhe uma política explícita e uma bateria de casos de teste.
O Effect Schema também gera JSON Schema (descrição de dados consumível por outras ferramentas), no formato 2020-12. Isso ajuda quando o contrato precisa alimentar documentação ou integrações. Regras executáveis e transformações ainda exigem atenção ao que o formato consegue representar.
Standard Schema: uma função para as cinco
Standard Schema é uma interface comum para bibliotecas de validação. Ela expõe ~standard.validate, que recebe unknown e retorna um valor validado ou problemas encontrados. O retorno pode ser síncrono ou assíncrono, por isso a função compartilhada usa await.
import type { StandardSchemaV1 } from "@standard-schema/spec";
export async function validar<Saida>(
esquema: StandardSchemaV1<unknown, Saida>,
entrada: unknown,
): Promise<Saida> {
const resultado = await esquema["~standard"].validate(entrada);
if (resultado.issues) {
const mensagem = resultado.issues
.map((falha) => falha.message)
.join("; ");
throw new Error(mensagem);
}
return resultado.value;
}O parâmetro genérico Saida representa o tipo que cada esquema entrega. import type importa apenas a definição usada pelo compilador, sem acrescentar uma dependência executável por causa desse import. A função depende do contrato Standard Schema, sem consultar classes de erro específicas das bibliotecas.
Zod, Zod Mini, Valibot e ArkType podem fornecer seus esquemas diretamente. Para Effect Schema, use o valor produzido por Schema.toStandardSchemaV1. O exemplo abaixo apresenta as cinco chamadas equivalentes para comparação; em uma aplicação, escolha a implementação necessária.
import { validar } from "./validar";
import { esquemaLead as zod } from "./lead-zod";
import { esquemaLead as mini } from "./lead-zod-mini";
import { esquemaLead as valibot } from "./lead-valibot";
import { esquemaLead as arktype } from "./lead-arktype";
import { esquemaPadrao as effect } from "./lead-effect";
export async function comparar(entrada: unknown) {
const leadZod = await validar(zod, entrada);
const leadMini = await validar(mini, entrada);
const leadValibot = await validar(valibot, entrada);
const leadArkType = await validar(arktype, entrada);
const leadEffect = await validar(effect, entrada);
return { leadZod, leadMini, leadValibot, leadArkType, leadEffect };
}Esse contrato compartilha validação e inferência, mas cada biblioteca conserva suas mensagens e regras de processamento. Em interfaces, adapte a função para devolver os problemas com seus caminhos, em vez de juntar tudo em uma exceção. O caminho identifica o campo, inclusive dentro de objetos e listas.
Escolha pela integração e pelo código entregue
O tamanho final depende dos recursos importados e do empacotador (ferramenta que reúne e otimiza os arquivos). Mensagens também precisam de uma política da Mirante: vocabulário, idioma e apresentação por campo. Para exportar JSON Schema, confira se cada recurso usado pelo esquema possui representação equivalente.
| Biblioteca | Estilo de API | Tree shaking | Mensagens | JSON Schema | Quando escolher |
|---|---|---|---|---|---|
| Zod 4 | Objetos e métodos encadeados | Meça os recursos incluídos | Personalização e configuração compartilhada | Conversão nativa, com limites por recurso | Equipe que prefere encadeamento e integrações com Zod |
| Zod Mini | Funções e check | API desenhada para favorecer remoção de código | Infraestrutura de erros do Zod | Conversão nativa, com limites por recurso | Interface com atenção ao código enviado ao navegador |
| Valibot 1.5 | Funções modulares e pipe | Composição favorece remoção de código | Mensagens por ação e configuração compartilhada | Conversor separado | Equipe que prefere regras em sequência e imports modulares |
| ArkType 2.2 | DSL em strings e objetos | Meça o resultado da integração | Coleção de erros com resumo e personalização | Conversão com limites por recurso | Equipe confortável com restrições escritas como tipos |
| Effect Schema | Estruturas e verificações compostas | Depende dos imports e do empacotamento | Anotações e formatação de problemas | Geração em 2020-12 | Projeto que já usa Effect ou precisa compor contratos nesse ecossistema |
Valide antes de causar efeitos
Na captura de leads da Mirante, valide antes de gravar o cadastro, disparar uma integração ou alterar o orçamento da campanha. No navegador, essa etapa melhora o retorno do formulário; no servidor, ela precisa acontecer novamente, pois qualquer cliente pode enviar dados diferentes. Um valor recuperado do localStorage também passa pela mesma fronteira.
No webhook, a validação do corpo deve acompanhar a verificação de autenticidade do remetente. Um objeto com os campos corretos ainda precisa das permissões e regras de negócio aplicáveis à operação. Para o aceite, true descreve o valor recebido; o registro de consentimento precisa guardar o contexto exigido pelo processo da agência.
Props tipadas: do botão ao formulário de lead
Props que preservam o botão nativo
No React 19 (biblioteca para construir interfaces), as props (propriedades recebidas pelo componente) podem ser descritas com type, uma declaração de tipo do TypeScript. Para o botão da Mirante, uma união literal (conjunto fechado de valores) limita as variantes visuais. Já ReactNode representa o conteúdo que o React consegue renderizar, como texto, elementos e listas de elementos.
ComponentProps<"button"> extrai as propriedades aceitas pelo botão nativo, incluindo onClick, disabled, type e ref. A interseção com & combina essas propriedades com as específicas do componente. Assim, novas opções visuais convivem com os recursos do elemento HTML.
import type { ComponentProps, ReactNode } from "react";
export type Variante = "primario" | "secundario";
export type BotaoProps = ComponentProps<"button"> & {
variante?: Variante;
children: ReactNode;
};
export function Botao({
variante = "primario",
children,
type = "button",
ref,
...propriedades
}: BotaoProps) {
return (
<button
{...propriedades}
ref={ref}
type={type}
data-variante={variante}
>
{children}
</button>
);
}O operador ... repassa as propriedades restantes ao elemento, preservando os eventos e atributos recebidos. O atributo data-variante serve de seletor para os estilos, enquanto o valor padrão type="button" evita submissões acidentais dentro de formulários. Para enviar o formulário, passe type="submit" explicitamente.
No React 19, ref (referência ao elemento renderizado) pode ser recebida diretamente como prop por um componente de função. O tipo herdado de ComponentProps<"button"> já inclui essa referência. O exemplo dispensa forwardRef, a função usada para encaminhar referências em componentes de versões anteriores.
Um componente com as, dois contratos
Um componente polimórfico (que pode renderizar elementos diferentes) precisa ajustar suas props ao elemento escolhido. Aqui, as funciona como discriminante: uma propriedade cujo valor determina o formato aceito para o restante do objeto. A união permite um botão com disabled ou um link com href obrigatório.
import type { ComponentProps, ReactNode } from "react";
import type { Variante } from "./botao";
type Aparencia = {
variante?: Variante;
children: ReactNode;
};
type ControleProps =
| (Aparencia & ComponentProps<"button"> & { as?: "button" })
| (Aparencia & ComponentProps<"a"> & { as: "a"; href: string });
export function Controle(propriedades: ControleProps) {
if (propriedades.as === "a") {
const { as, variante = "primario", children, ...resto } = propriedades;
return (
<a {...resto} data-variante={variante}>
{children}
</a>
);
}
const {
as, variante = "primario", children, type = "button", ...resto
} = propriedades;
return (
<button {...resto} type={type} data-variante={variante}>
{children}
</button>
);
}O narrowing (refinamento do tipo após uma condição) acontece antes de separar as propriedades de cada ramo. Isso preserva os tipos específicos de eventos e referências: no link, a referência aponta para HTMLAnchorElement; no botão, para HTMLButtonElement. A propriedade as é retirada antes do repasse e fica fora do HTML renderizado.
<Controle disabled>Salvar lead</Controle>aceita propriedades de botão.<Controle as="a" href="/campanhas">Ver campanhas</Controle>aceita propriedades de link.- Combinar
as="a"comdisabledproduz erro de tipo, pois links não possuem esse atributo.
A escolha do elemento acompanha sua função: botões executam ações, links navegam para destinos. Um link visualmente desabilitado exige tratamento próprio de interação e acessibilidade. Para manter este contrato simples, a variante de link continua sendo um destino navegável.
Tabela genérica com colunas existentes
Um genérico (tipo parametrizado), como Tabela<T>, permite reutilizar a estrutura com diferentes registros. O operador keyof T produz a união das chaves disponíveis em cada registro. Com isso, uma coluna de campanhas só pode apontar para uma propriedade existente na campanha.
import type { Key, ReactNode } from "react";
type Coluna<T> = {
chave: keyof T;
titulo: string;
renderizar?: (linha: T) => ReactNode;
};
type TabelaProps<T> = {
linhas: readonly T[];
colunas: readonly Coluna<T>[];
obterChave: (linha: T) => Key;
};
export function Tabela<T>({ linhas, colunas, obterChave }: TabelaProps<T>) {
return (
<table>
<thead>
<tr>{colunas.map((coluna) => (
<th key={String(coluna.chave)} scope="col">{coluna.titulo}</th>
))}</tr>
</thead>
<tbody>
{linhas.map((linha) => (
<tr key={obterChave(linha)}>
{colunas.map((coluna) => (
<td key={String(coluna.chave)}>
{coluna.renderizar
? coluna.renderizar(linha)
: String(linha[coluna.chave] ?? "")}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}A conversão com String oferece uma apresentação básica para valores sem formatação específica. Para orçamento em centavos, a coluna pode fornecer uma função que recebe a linha inteira e devolve conteúdo renderizável. O tipo dessa linha continua sendo o registro original, sem precisar de conversões forçadas.
import { Tabela } from "./tabela";
type Campanha = {
id: string;
nome: string;
orcamentoCentavos: number;
};
const campanhas: Campanha[] = [
{ id: "mirante-01", nome: "Captura de leads", orcamentoCentavos: 150000 },
];
const moeda = new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
});
export function PainelCampanhas() {
return (
<Tabela
linhas={campanhas}
obterChave={(campanha) => campanha.id}
colunas={[
{ chave: "nome", titulo: "Campanha" },
{
chave: "orcamentoCentavos",
titulo: "Orçamento",
renderizar: (campanha) =>
moeda.format(campanha.orcamentoCentavos / 100),
},
]}
/>
);
}O TypeScript infere T a partir de linhas, então escrever chave: "receita" nesse painel causa erro. A função obterChave fornece uma identidade estável para cada linha, usada pelo React para acompanhar os elementos. O contrato considera uma coluna por chave; para repetir o mesmo campo em colunas diferentes, acrescente um identificador próprio à coluna.
Estado e eventos do formulário
Os hooks (funções do React que conectam componentes a recursos como estado) useState e useReducer atendem a necessidades diferentes. useState funciona bem para uma união curta de estados; useReducer concentra alterações relacionadas em uma função redutora. Nas ações discriminadas, a propriedade tipo determina quais dados acompanham cada alteração.
"use client";
import { useReducer, useState, type ChangeEvent } from "react";
import type * as React from "react";
type Dados = { nome: string; email: string };
type Estado = Dados & { erro: string };
type Acao =
| { tipo: "editar"; campo: keyof Dados; valor: string }
| { tipo: "falhar"; mensagem: string }
| { tipo: "limpar" };
const inicial: Estado = { nome: "", email: "", erro: "" };
function reduzir(estado: Estado, acao: Acao): Estado {
switch (acao.tipo) {
case "editar":
return { ...estado, [acao.campo]: acao.valor, erro: "" };
case "falhar":
return { ...estado, erro: acao.mensagem };
case "limpar":
return inicial;
}
}
export function FormularioLocal() {
const [estado, enviar] = useReducer(reduzir, inicial);
const [status, definirStatus] = useState<"editando" | "pronto">("editando");
const alterar = (campo: keyof Dados) => (evento: ChangeEvent<HTMLInputElement>) => {
enviar({ tipo: "editar", campo, valor: evento.currentTarget.value });
definirStatus("editando");
};
function preparar(evento: React.FormEvent<HTMLFormElement>) {
evento.preventDefault();
if (!estado.email.trim()) return enviar({ tipo: "falhar", mensagem: "Informe o e-mail." });
definirStatus("pronto");
}
return <form onSubmit={preparar}>
<input aria-label="Nome" value={estado.nome} onChange={alterar("nome")} />
<input aria-label="E-mail" value={estado.email} onChange={alterar("email")} />
<button type="submit">Preparar lead</button>
<p aria-live="polite">{estado.erro || status}</p>
</form>;
}ChangeEvent<HTMLInputElement> descreve um evento de alteração de campo, e React.FormEvent<HTMLFormElement> descreve o evento do formulário. Em ambos, currentTarget aponta para o elemento ao qual o manipulador foi associado. O método preventDefault() cancela o envio padrão do navegador neste exemplo de preparação local, que ainda não persiste o lead.
A anotação em useState<"editando" | "pronto"> impede atribuições fora desses dois valores. No redutor, acessar acao.mensagem só funciona no ramo "falhar", pois os outros formatos de ação possuem propriedades diferentes. A verificação local de preenchimento melhora a interação, mas os dados recebidos pelo servidor também precisam de validação.
Server Action tipada no Next.js 16
Uma Server Action (função executada no servidor e acionável pelo formulário) permite concentrar validação e integração no Next.js 16. O hook useActionState conecta essa função ao estado devolvido e ao indicador de envio pendente. Sua função recebe primeiro o estado anterior e depois o FormData, objeto que reúne os campos enviados.
O exemplo usa Zod (biblioteca de validação de dados em execução) para conferir nome e e-mail antes de chamar a integração da Mirante. A variável de ambiente MIRANTE_LEADS_URL deve conter o endereço privado do webhook (endpoint que recebe notificações HTTP). O estado de sucesso só é retornado após uma resposta bem-sucedida desse serviço.
"use server";
import { z } from "zod";
export type EstadoLead =
| { status: "inicial" }
| { status: "erro"; mensagem: string }
| { status: "sucesso"; mensagem: string };
const esquema = z.object({
nome: z.string().trim().min(1),
email: z.email(),
});
export async function registrarLead(
_estadoAnterior: EstadoLead,
formulario: FormData,
): Promise<EstadoLead> {
const resultado = esquema.safeParse({
nome: formulario.get("nome"),
email: formulario.get("email"),
});
if (!resultado.success) {
return { status: "erro", mensagem: "Confira o nome e o e-mail." };
}
const destino = process.env.MIRANTE_LEADS_URL;
if (!destino) return { status: "erro", mensagem: "Integração indisponível." };
try {
const resposta = await fetch(destino, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(resultado.data),
});
if (!resposta.ok) throw new Error("Falha na integração");
return { status: "sucesso", mensagem: "Lead recebido pela integração." };
} catch {
return { status: "erro", mensagem: "Falha no envio. Tente novamente." };
}
}A diretiva "use server" identifica o módulo de funções executadas no servidor; "use client" marca a entrada do componente que usa hooks no cliente. O tipo EstadoLead atravessa essa divisão como informação de compilação, sem enviar código de validação ao navegador. A tipagem de retorno obriga a ação a devolver um dos estados previstos em todos os caminhos.
"use client";
import { useActionState } from "react";
import { Botao } from "./botao";
import { registrarLead, type EstadoLead } from "./acoes";
const estadoInicial: EstadoLead = { status: "inicial" };
export function FormularioLead() {
const [estado, acao, pendente] = useActionState<EstadoLead, FormData>(
registrarLead,
estadoInicial,
);
return (
<form action={acao}>
<label>
Nome
<input name="nome" autoComplete="name" required />
</label>
<label>
E-mail
<input name="email" type="email" autoComplete="email" required />
</label>
<Botao type="submit" disabled={pendente}>
{pendente ? "Enviando..." : "Capturar lead"}
</Botao>
<p aria-live="polite">
{estado.status !== "inicial" ? estado.mensagem : ""}
</p>
</form>
);
}Os atributos name determinam as chaves disponíveis em formulario.get, e required oferece a verificação inicial do navegador. Neste fluxo, action={acao} encaminha o envio à ação, sem o manipulador local com preventDefault(). O atributo aria-live="polite" permite anunciar a mensagem atualizada às tecnologias assistivas sem interromper uma leitura em andamento.
Uma Server Action continua sendo uma entrada de dados externos, mesmo quando o formulário possui tipos. Em produção, aplique autenticação e autorização quando necessárias, proteção contra envios abusivos e tratamento de duplicatas na integração. Desabilitar o botão durante o envio melhora a interação, enquanto o servidor mantém a responsabilidade pelas regras de aceitação do lead.
Do formulário ao servidor: tipos para criar leads
Uma API (interface de comunicação entre sistemas) pode compartilhar tipos entre o formulário da Mirante e o servidor. No endpoint (operação acessível por um endereço) de criação de lead, isso permite conferir campos enviados e respostas esperadas durante o desenvolvimento. TypeScript, a linguagem que acrescenta verificação estática de tipos ao JavaScript, faz essa conferência antes da execução.
Dados recebidos pela rede também precisam de validação em execução: conferir o conteúdo enquanto o programa roda. Um cliente tipado ajuda quem escreve a integração; a validação protege a entrada do servidor, inclusive quando outra aplicação chama a mesma operação.
Um contrato para a entrada e a resposta
O contrato (descrição dos dados aceitos e devolvidos) será igual nos três exemplos de criação de lead. Zod, biblioteca de validação com inferência de tipos, descreve esse contrato por meio de schemas (esquemas que verificam a estrutura dos dados). O nome precisa ter conteúdo após a remoção dos espaços nas extremidades, e o e-mail precisa passar pela validação de formato.
import { z } from 'zod';
export const entradaLead = z.object({
nome: z.string().trim().min(1),
email: z.email(),
});
export const leadCriado = entradaLead.extend({
id: z.uuid(),
});O identificador usa UUID (formato padronizado de identificador). Os exemplos geram esse valor e devolvem o objeto, sem persistência em banco de dados. Em produção, a gravação entra depois da validação e antes da resposta de sucesso.
tRPC 11: o cliente acompanha o roteador
tRPC oferece RPC (chamada de procedimento remoto) com tipos compartilhados entre cliente e servidor. O router (agrupador de operações) reúne procedures (operações individuais), e .input() valida a entrada com Zod. Uma mutation (operação destinada a alterar dados) representa a criação do lead.
import { initTRPC } from '@trpc/server';
import { entradaLead, leadCriado } from './lead.js';
const trpc = initTRPC.create();
export const roteador = trpc.router({
criarLead: trpc.procedure
.input(entradaLead)
.output(leadCriado)
.mutation(({ input }) => ({
id: crypto.randomUUID(),
...input,
})),
});
export type Roteador = typeof roteador;Monte esse roteador em /trpc usando o adaptador do ambiente de execução, a ponte entre a aplicação e as requisições recebidas. No cliente, import type importa apenas a informação de tipos, sem carregar a implementação do servidor. O transporte (mecanismo que envia as chamadas) abaixo usa HTTP, protocolo de requisição e resposta da Web.
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { Roteador } from './trpc-servidor.js';
const cliente = createTRPCClient<Roteador>({
links: [
httpBatchLink({ url: 'https://api.mirante.example/trpc' }),
],
});
const lead = await cliente.criarLead.mutate({
nome: 'Ana',
email: 'ana@mirante.example',
});
console.log(lead.id);O editor conhece os campos da entrada e o tipo de lead.id. Alterar o contrato no servidor faz os consumidores incompatíveis aparecerem na checagem de tipos. Esse compartilhamento funciona bem quando a equipe controla as duas pontas e pode coordenar suas atualizações.
Hono 4: os tipos acompanham as rotas HTTP
Hono RPC extrai os tipos das rotas declaradas no servidor. O middleware (função executada durante o processamento da requisição) zValidator valida o corpo JSON, formato textual usado para trocar dados estruturados. A chamada encadeada de .post() preserva no tipo de app a descrição da rota.
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { entradaLead } from './lead.js';
export const app = new Hono().post(
'/leads',
zValidator('json', entradaLead),
(contexto) => {
const entrada = contexto.req.valid('json');
const lead = {
id: crypto.randomUUID(),
...entrada,
};
return contexto.json(lead, 201);
},
);hc<typeof app> cria o cliente com base nesse tipo, sem gerar um arquivo de cliente separado. A resposta também depende do status HTTP (código que indica o resultado da requisição). Conferir 201 permite tratar especificamente a criação bem-sucedida.
import { hc } from 'hono/client';
import type { app } from './hono-servidor.js';
const cliente = hc<typeof app>('https://api.mirante.example');
const resposta = await cliente.leads.$post({
json: {
nome: 'Ana',
email: 'ana@mirante.example',
},
});
if (resposta.status !== 201) {
throw new Error('Não foi possível criar o lead');
}
const lead = await resposta.json();
console.log(lead.id);Preserve o resultado encadeado das declarações ao organizar os arquivos. Se a variável exportada perder os tipos específicos das rotas, o cliente também perde essa informação. Aqui, a entrada recebe validação em execução; o tipo inferido da resposta, sozinho, não acrescenta um validador à saída.
oRPC: contrato explícito com OpenAPI
OpenAPI é uma especificação para descrever operações HTTP, entradas e respostas de uma API. No oRPC, o contrato pode nascer separado da implementação, com método e caminho explícitos. Isso ajuda quando a Mirante atende aplicações próprias e integrações mantidas por outras equipes.
import { oc } from '@orpc/contract';
import { entradaLead, leadCriado } from './lead.js';
export const contrato = {
criarLead: oc
.route({
method: 'POST',
path: '/leads',
summary: 'Criar lead da Mirante',
})
.input(entradaLead)
.output(leadCriado),
};implement() vincula os handlers (funções que atendem às operações) ao contrato. OpenAPIHandler expõe essas operações com a representação HTTP compatível com OpenAPI. O servidor hospedeiro deve encaminhar as requisições para esse adaptador e devolver a resposta produzida por ele.
import { implement } from '@orpc/server';
import { OpenAPIHandler } from '@orpc/openapi/fetch';
import { contrato } from './orpc-contrato.js';
const implementacao = implement(contrato);
export const roteador = implementacao.router({
criarLead: implementacao.criarLead.handler(({ input }) => ({
id: crypto.randomUUID(),
...input,
})),
});
export const handler = new OpenAPIHandler(roteador);Para uma aplicação TypeScript, o cliente pode usar diretamente o contrato. Para consumidores em outras linguagens, gere o documento OpenAPI com as ferramentas do oRPC e a conversão dos schemas Zod. A publicação do documento e a geração de clientes externos são etapas próprias do projeto.
import { createORPCClient } from '@orpc/client';
import type { ContractRouterClient } from '@orpc/contract';
import { OpenAPILink } from '@orpc/openapi-client/fetch';
import { contrato } from './orpc-contrato.js';
const transporte = new OpenAPILink(contrato, {
url: 'https://api.mirante.example',
});
const cliente: ContractRouterClient<typeof contrato> =
createORPCClient(transporte);
const lead = await cliente.criarLead({
nome: 'Ana',
email: 'ana@mirante.example',
});
console.log(lead.id);Escolha pelo limite que precisa de tipos
Cliente inferido significa que o compilador calcula seus tipos a partir do servidor ou do contrato. Cliente gerado significa que uma ferramenta escreve arquivos a partir de uma descrição, como OpenAPI. As duas abordagens ainda exigem cuidado com versões publicadas: um navegador com código antigo pode chamar um servidor já atualizado.
| Ferramenta | Onde o tipo nasce | Cliente gerado? | OpenAPI? | Quando escolher |
|---|---|---|---|---|
| tRPC 11 | Roteador, schemas e retornos das procedures | Não exige geração; cliente inferido | Não no núcleo | Aplicações TypeScript com cliente e servidor coordenados |
| Hono 4 RPC | Declarações encadeadas das rotas e validadores | Não exige geração; hc usa o tipo da aplicação | Por integração específica | API HTTP em Hono com consumidores TypeScript |
| oRPC | Contrato explícito ou implementação | Cliente tipado sem geração; geração externa via OpenAPI | Sim | Contrato HTTP compartilhado e consumidores variados |
| TanStack Router | Árvore de rotas e validadores de parâmetros e busca | Não gera cliente de API | Não se aplica à navegação | Navegação tipada em aplicações React |
| Next.js 16 | Estrutura de arquivos e tipos gerados de rotas | Gera tipos de rotas, sem cliente de API | Não automaticamente | Aplicações Next.js com links e propriedades de página tipados |
TanStack Router: valide o endereço da campanha
Uma rota de página descreve a navegação da interface. Seus params (parâmetros inseridos no caminho) e search params (valores da consulta após ?) também recebem dados externos, vindos da barra de endereço ou de links. No TanStack Router, a rota pode validar ambos antes de usá-los no painel.
O exemplo usa roteamento por arquivos, com a árvore de rotas gerada e registrada na aplicação. O componente (função que descreve parte da interface) recebe os valores já processados pelos validadores. Link usa os caminhos conhecidos para conferir o destino durante o desenvolvimento.
import { createFileRoute, Link } from '@tanstack/react-router';
import { z } from 'zod';
const buscaCampanha = z.object({
pagina: z.coerce.number().int().positive().default(1),
});
export const Route = createFileRoute('/campanhas/$campanhaId')({
params: {
parse: (parametros) => ({
campanhaId: z.uuid().parse(parametros.campanhaId),
}),
stringify: (parametros) => ({
campanhaId: parametros.campanhaId,
}),
},
validateSearch: (busca: Record<string, unknown>) =>
buscaCampanha.parse(busca),
component: Painel,
});
function Painel() {
const { campanhaId } = Route.useParams();
const { pagina } = Route.useSearch();
return (
<Link
to="/campanhas/$campanhaId"
params={{ campanhaId }}
search={{ pagina: pagina + 1 }}
>
Abrir página {pagina + 1} da campanha
</Link>
);
}z.coerce.number() tenta converter o valor recebido para número antes de aplicar as demais regras. Sem pagina, o painel usa 1; uma página inválida faz a validação falhar. Configure a apresentação desses erros na rota para orientar quem abriu um endereço incorreto.
Next.js 16: links conferidos e propriedades geradas
No Next.js, typedRoutes habilita a conferência dos destinos reconhecidos em links. Já PageProps, um tipo auxiliar global, descreve as propriedades recebidas por uma página com base em seu caminho. Esses recursos ajudam na navegação e na implementação das páginas; a tipagem do endpoint de criação de lead continua dependendo do contrato escolhido.
import type { NextConfig } from 'next';
const configuracao: NextConfig = {
typedRoutes: true,
};
export default configuracao;Execute next typegen para gerar os tipos sem precisar executar uma compilação de produção completa. Depois, rode a checagem de TypeScript com tsc --noEmit, que verifica os arquivos sem emitir JavaScript. Essa ordem também serve para a integração contínua, a execução automatizada das verificações do projeto.
# Gere os tipos das rotas.
npx next typegen
# Confira o projeto sem emitir JavaScript.
npx tsc --noEmitEm PageProps, os parâmetros de caminho e de busca são assíncronos e devem ser aguardados. O tipo gerado conhece o nome campanhaId, mas o formato UUID ainda precisa de validação em execução. A busca também precisa de regras próprias para representar uma página válida.
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { z } from 'zod';
const parametrosCampanha = z.object({
campanhaId: z.uuid(),
});
const buscaCampanha = z.object({
pagina: z.string().regex(/^[1-9]\d*$/).default('1'),
});
export default async function Painel(
propriedades: PageProps<'/campanhas/[campanhaId]'>,
) {
const parametros = parametrosCampanha.safeParse(
await propriedades.params,
);
if (!parametros.success) notFound();
const busca = buscaCampanha.safeParse(
await propriedades.searchParams,
);
if (!busca.success) return <p>Informe uma página válida.</p>;
const { campanhaId } = parametros.data;
return (
<div>
<p>Página {busca.data.pagina}</p>
<Link href={`/campanhas/${campanhaId}`}>Abrir campanha</Link>
</div>
);
}notFound() encerra a renderização com o tratamento de página não encontrada. A validação da busca rejeita valores repetidos recebidos como listas e textos fora do padrão definido. A autorização, que decide quais campanhas cada pessoa pode acessar, deve ocorrer antes de consultar ou exibir seus dados.
Webhook recebido: valide antes de usar
Um webhook é uma chamada enviada por outro sistema quando acontece um evento. Mesmo que o fornecedor publique tipos TypeScript, trate o corpo recebido como unknown, tipo que exige verificação antes do uso. safeParse() devolve um resultado de sucesso ou falha sem lançar uma exceção de validação.
O trecho abaixo pressupõe que a autenticidade do remetente já foi conferida. Se o fornecedor usa assinatura criptográfica, confira-a sobre os bytes originais conforme o protocolo dele, antes de consumir e transformar o corpo. Um evento com estrutura válida ainda pode ter sido enviado por alguém sem autorização.
import { z } from 'zod';
import { leadCriado } from './lead.js';
const eventoLead = z.object({
evento: z.literal('lead.criado'),
lead: leadCriado,
});
export async function receberWebhook(requisicao: Request) {
let corpo: unknown;
try {
corpo = await requisicao.json();
} catch {
return Response.json({ erro: 'JSON inválido' }, { status: 400 });
}
const resultado = eventoLead.safeParse(corpo);
if (!resultado.success) {
return Response.json({ erro: 'Evento inválido' }, { status: 400 });
}
const { lead } = resultado.data;
return Response.json({ recebido: true, leadId: lead.id });
}Antes de disparar uma campanha ou registrar uma conversão, aplique idempotência (controle para que o mesmo evento não produza o efeito repetidamente). Use o identificador de evento fornecido pela integração e registre seu processamento de forma persistente. Também limite o tamanho do corpo recebido e evite registrar e-mails completos em mensagens de erro ou registros operacionais.
Dados do painel: do lead salvo à métrica tipada
Drizzle: o esquema também produz os tipos
Um esquema descreve tabelas, colunas e relações do banco. No Drizzle ORM (biblioteca que conecta objetos do código a registros), pgTable declara uma tabela do PostgreSQL. A Mirante pode manter campanhas e leads no mesmo arquivo, com uma chave estrangeira (referência a um registro de outra tabela) ligando cada lead à campanha.
import { pgTable, integer, text, timestamp } from "drizzle-orm/pg-core";
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
export const campanhas = pgTable("campanhas", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
nome: text("nome").notNull(),
orcamentoCentavos: integer("orcamento_centavos").notNull(),
});
export const leads = pgTable("leads", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
campanhaId: integer("campanha_id")
.notNull()
.references(() => campanhas.id),
email: text("email").notNull(),
criadoEm: timestamp("criado_em", { withTimezone: true, mode: "date" })
.defaultNow()
.notNull(),
});
export type Campanha = InferSelectModel<typeof campanhas>;
export type NovaCampanha = InferInsertModel<typeof campanhas>;
export type Lead = InferSelectModel<typeof leads>;
export type NovoLead = InferInsertModel<typeof leads>;
export const novoLead: NovoLead = {
campanhaId: 12,
email: "contato@mirante.example",
};InferSelectModel extrai o formato dos registros lidos. InferInsertModel extrai o formato aceito na inserção: campos gerados pelo banco, como id e criadoEm, podem ser omitidos. A coluna de orçamento usa um inteiro; confira o limite dessa coluna antes de aceitar valores altos.
Esses tipos verificam o código durante a compilação, etapa que analisa o programa antes da execução. Dados recebidos por formulário ou webhook (chamada HTTP enviada por outro serviço) ainda precisam de validação em execução. O esquema também precisa ser aplicado ao banco por uma migração, isto é, uma alteração versionada da estrutura.
Prisma 7: gerar o cliente a partir do esquema
No Prisma 7, o esquema alimenta um gerador de cliente, que produz funções de consulta e tipos. O gerador prisma-client exige uma saída explícita e funciona sem a engine de consultas em Rust. Os modelos abaixo representam as mesmas tabelas; @map e @@map associam nomes do código aos nomes existentes no banco.
mkdir -p prisma
cat > prisma/schema.prisma <<'PRISMA'
generator client {
provider = "prisma-client"
output = "../src/gerado/prisma"
}
datasource db {
provider = "postgresql"
}
model Campanha {
id Int @id @default(autoincrement())
nome String
orcamentoCentavos Int @map("orcamento_centavos")
leads Lead[]
@@map("campanhas")
}
model Lead {
id Int @id @default(autoincrement())
campanhaId Int @map("campanha_id")
email String
criadoEm DateTime @default(now()) @map("criado_em") @db.Timestamptz(6)
campanha Campanha @relation(fields: [campanhaId], references: [id])
@@map("leads")
}
PRISMA
npx prisma generateA conexão usada pelas ferramentas do Prisma fica em prisma.config.ts. A variável de ambiente DATABASE_URL guarda a URL de acesso ao banco. Gerar o cliente atualiza os arquivos TypeScript; aplicar alterações no banco continua sendo uma operação separada.
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
url: env("DATABASE_URL"),
},
});Na aplicação, o adaptador de driver (ponte com o cliente PostgreSQL) fornece a conexão ao Prisma. O tipo Lead representa o registro completo; Prisma.LeadGetPayload permite descrever o resultado de uma seleção específica. A própria chamada de consulta também infere esse resultado, sem exigir uma anotação manual.
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../gerado/prisma/client";
import type { Lead, Prisma } from "../gerado/prisma/client";
const conexao = process.env.DATABASE_URL;
if (!conexao) throw new Error("DATABASE_URL ausente");
const adaptador = new PrismaPg({ connectionString: conexao });
export const banco = new PrismaClient({ adapter: adaptador });
export type LeadSalvo = Lead;
export type LeadDoPainel = Prisma.LeadGetPayload<{
select: { id: true; email: true; campanha: { select: { nome: true } } };
}>;
export async function listarLeads() {
return banco.lead.findMany({
select: {
id: true,
email: true,
campanha: { select: { nome: true } },
},
});
}Escolha uma ferramenta para conduzir as migrações de cada conjunto de tabelas. Os exemplos de Drizzle e Prisma são alternativas para o mesmo domínio. Manter dois históricos independentes de alterações sobre as mesmas tabelas exige coordenação adicional.
Kysely: consultar com o formato do banco
O Kysely é um construtor de consultas tipadas: os nomes de tabelas e colunas orientam as operações disponíveis. Sua interface de banco descreve os valores que o driver entrega, sem criar tabelas nem converter respostas automaticamente. Generated marca colunas cujo valor pode ser gerado na inserção.
import type { Generated, Kysely } from "kysely";
interface Banco {
campanhas: {
id: Generated<number>;
nome: string;
orcamento_centavos: number;
};
leads: {
id: Generated<number>;
campanha_id: number;
email: string;
criado_em: Generated<Date>;
};
}
export async function listarLeadsDaCampanha(
banco: Kysely<Banco>,
campanhaId: number,
) {
return banco
.selectFrom("leads")
.innerJoin("campanhas", "campanhas.id", "leads.campanha_id")
.select(["leads.id", "leads.email", "campanhas.nome as campanha"])
.where("campanhas.id", "=", campanhaId)
.execute();
}O resultado contém somente id, email e campanha, com tipos inferidos pela seleção. A declaração de criado_em como Date pressupõe que o driver esteja configurado para devolver esse objeto. Se ele entregar texto, ajuste a interface ou transforme o valor na entrada.
Dinheiro: centavos com uma marca de tipo
Guardar dinheiro em centavos permite usar inteiros nas operações básicas. Um branded type (tipo com uma marca exclusiva) diferencia esse valor de outros números, como quantidade de leads. A marca existe para o compilador; uma função de criação verifica o valor durante a execução.
declare const marcaCentavos: unique symbol;
export type Centavos = number & {
readonly [marcaCentavos]: true;
};
export function emCentavos(valor: number): Centavos {
if (!Number.isSafeInteger(valor) || valor < 0) {
throw new Error("Informe centavos inteiros, seguros e não negativos");
}
return valor as Centavos;
}
const orcamento = emCentavos(250_000);
const acrescimo = emCentavos(35_000);
const total = emCentavos(orcamento + acrescimo);
export function formatarDinheiro(valor: Centavos): string {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(valor / 100);
}
export const textoDoOrcamento = formatarDinheiro(total);- resultado
sem erro de tipo, com strict
O operador de soma devolve number, então o resultado passa novamente pela função de criação. Essa checagem cobre a segurança do inteiro em JavaScript; o limite da coluna do banco também precisa ser respeitado. Para reembolsos ou saldos negativos, defina outra política de validação.
Campos numéricos lidos pelo ORM chegam como number. Converta orcamentoCentavos com emCentavos ao montar o objeto usado pelo painel. A mesma regra vale para dados recebidos de integrações, sem aplicar uma asserção as Centavos diretamente sobre a entrada.
Datas: instante, transporte e dia de relatório
Date representa um instante com precisão de milissegundos. Uma string ISO (texto em formato padronizado de data e hora) facilita o transporte em JSON, formato textual usado entre aplicações. Para instantes, prefira uma representação com fuso explícito e normalize a saída com toISOString(), que usa UTC, referência global de horário.
export function serializarData(data: Date): string {
if (Number.isNaN(data.getTime())) {
throw new Error("Data inválida");
}
return data.toISOString();
}
const criadoEm = new Date("2026-11-12T09:30:00-03:00");
const criadoEmIso = serializarData(criadoEm);
export const resposta = {
email: "contato@mirante.example",
criadoEm: criadoEmIso,
};- resultado
sem erro de tipo, com strict
Uma string ISO continua sendo string para o TypeScript. Valide o formato ao receber dados externos e defina se o campo aceita um instante ou apenas uma data de calendário. Extrair os primeiros caracteres de um instante em UTC pode produzir um dia diferente daquele usado pela equipe da Mirante.
Temporal (API que separa instantes, datas de calendário e horários com fuso) ajuda a explicitar essas escolhas. Temporal.Instant serve para um acontecimento na linha do tempo; Temporal.PlainDate, para uma data sem horário; Temporal.ZonedDateTime, para um instante associado a um fuso. Os tipos estão disponíveis com lib: ["es2025"], mas essa configuração fornece declarações ao compilador: confira o suporte do ambiente de execução ou forneça uma implementação compatível.
Agrupar leads e montar séries
Uma série de gráfico é uma sequência de pontos com formato conhecido. Antes de agrupar por dia, calcule o dia de relatório no fuso escolhido para o painel. O exemplo já recebe esse campo normalizado, evitando misturar a definição do calendário com a contagem.
A função genérica (função que preserva tipos recebidos) abaixo aceita qualquer lista de objetos. A restrição K extends keyof T limita a chave às propriedades de T. Map guarda grupos por chave, enquanto Set mantém valores únicos.
export function agruparPor<T, K extends keyof T>(
itens: readonly T[],
chave: K,
): Map<T[K], T[]> {
const grupos = new Map<T[K], T[]>();
for (const item of itens) {
const valor = item[chave];
const grupo = grupos.get(valor);
if (grupo) grupo.push(item);
else grupos.set(valor, [item]);
}
return grupos;
}
type LeadPainel = {
campanhaId: number;
dia: string;
email: string;
convertido: boolean;
};
export type PontoSerie = {
dia: string;
leads: number;
convertidos: number;
};
export function montarSerie(leads: readonly LeadPainel[]): PontoSerie[] {
const grupos = agruparPor(leads, "dia");
// ^?
return Array.from(grupos, ([dia, grupo]) => ({
dia,
leads: grupo.length,
convertidos: grupo.filter((lead) => lead.convertido).length,
})).sort((a, b) => a.dia.localeCompare(b.dia));
}
export function contarEmailsUnicos(leads: readonly LeadPainel[]): number {
return new Set(leads.map((lead) => lead.email)).size;
}- linha 28
const grupos: Map<string, LeadPainel[]> - resultado
sem erro de tipo, com strict
Em montarSerie, grupos tem tipo Map<string, LeadPainel[]>. A mesma função aceita "campanhaId" e produz chaves numéricas. A ordenação textual dos dias pressupõe o formato AAAA-MM-DD, com todos os pontos usando o mesmo calendário.
O Set conta textos distintos exatamente como foram recebidos. Defina uma política de normalização de e-mails antes de usá-lo para deduplicação. Dias sem leads também precisam ser acrescentados se o gráfico exigir uma sequência contínua.
Para volumes grandes, faça a agregação (redução de vários registros a totais) no banco e transporte apenas os pontos necessários. Confirme os tipos devolvidos por operações como contagem e soma no driver escolhido. Uma anotação TypeScript aplicada à consulta, sozinha, não transforma texto em número.
Métricas com unidades explícitas
O operador satisfies verifica se um objeto atende a um tipo, preservando sua inferência mais específica. Isso ajuda a conferir as métricas do painel e suas unidades. Um mapped type (tipo que percorre as chaves de outro tipo) associa cada valor à unidade correspondente.
import { emCentavos } from "../../dinheiro";
import type { Centavos } from "../../dinheiro";
type Valores = {
leads: number;
taxaConversao: number;
investimento: Centavos;
};
type Unidades = {
leads: "quantidade";
taxaConversao: "porCento";
investimento: "centavos";
};
type Metricas = {
[K in keyof Valores]: {
valor: Valores[K];
unidade: Unidades[K];
};
};
const capturados = 240;
const convertidos = 36;
export const metricas = {
leads: { valor: capturados, unidade: "quantidade" },
taxaConversao: {
valor: capturados === 0 ? 0 : (convertidos / capturados) * 100,
unidade: "porCento",
},
investimento: { valor: emCentavos(185_000), unidade: "centavos" },
} satisfies Metricas;Nesse contrato, a taxa usa a escala de zero a cem, e o investimento permanece em centavos até a formatação visual. A divisão por zero tem uma regra explícita: exibir zero quando não houver leads. Se o produto precisar distinguir ausência de dados de uma taxa realmente zerada, represente esse estado com null e trate a apresentação no componente.
Em volta do tsc: uma ferramenta por etapa
O compilador sozinho cobre só parte do trabalho. Um projeto da Mirante, seja um painel de métricas ou uma biblioteca de componentes de formulário, passa por sete etapas antes de chegar ao cliente. Cada etapa tem ferramentas melhores para ela, e misturar as funções costuma deixar o build lento e confuso.
A regra prática: uma ferramenta verifica os tipos, as outras só apagam os tipos. Transpilar (converter TypeScript em JavaScript removendo as anotações) é rápido justamente porque ninguém confere nada nesse passo. A conferência fica com o tsc.
O mapa por etapa
| Etapa | Opção | Quando escolher |
|---|---|---|
| Checar tipos | tsc --noEmit (TypeScript 7.0.2) | Padrão para qualquer projeto. O compilador nativo em Go roda várias vezes mais rápido que o 6.0. |
| Checar tipos | tsc do 6.0 ao lado do 7 | Quando alguma ferramenta depende da API programática ou de plugins de linguagem (Vue, Svelte, parte do Angular). |
| Transpilar | Node.js (type stripping) | Scripts, webhooks e serviços sem .tsx, sem enum e sem parameter properties. |
| Transpilar | Vite | Aplicações de front com .tsx, servidor de desenvolvimento e recarga rápida. |
| Empacotar biblioteca | tsdown 0.23 | Pacote para o npm com JavaScript e .d.ts (arquivos só de tipos) gerados juntos. |
| Empacotar biblioteca | tsc 7 emitindo .d.ts | Biblioteca simples que dispensa bundler (ferramenta que junta vários arquivos em poucos). |
| Lint | Oxlint 1.93 com --type-aware | Lint por tipos rápido, usando o motor do TypeScript 7 via oxlint-tsgolint. |
| Lint | typescript-eslint 8.72 | Regras sob medida ou plugins do ecossistema ESLint. Depende da API do compilador, então pode pedir o 6.0 instalado. |
| Lint | Biome 2.6 (biome lint) | Times que querem lint e formatação numa ferramenta só, sem regras por tipos. |
| Formatar | Biome 2.6 (biome format) | Formatação de TS, JS, JSON e CSS com um binário único. |
| Formatar | Biome 2.6 (biome ci) | No CI: confere formatação e lint sem reescrever arquivos. |
| Testar tipos | Vitest 5.1 (expectTypeOf, assertType) | Garantir que a API pública devolve os tipos prometidos. |
| Testar tipos | Vitest 5.1 com --typecheck | Rodar os arquivos .test-d.ts como suíte própria, separada dos testes de execução. |
| Publicar | attw --pack . (@arethetypeswrong/cli 0.18) | Antes de publicar no npm: confere se os tipos resolvem em ESM, CommonJS e bundlers. |
| Publicar | JSR | Publicar o próprio código TypeScript. O JSR gera os .d.ts para quem consome via npm. |
Duas observações evitam dor de cabeça. O Node apaga os tipos e executa, mas não lê o tsconfig.json nem acusa erro de tipo. E o Oxlint com --type-aware precisa do pacote oxlint-tsgolint instalado no projeto.
Os scripts no package.json
Cada etapa vira um script com nome curto. Assim o CI, o editor e a pessoa nova no time chamam exatamente os mesmos comandos.
{
"name": "@mirante/formularios",
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"lint": "oxlint --type-aware",
"format": "biome format --write .",
"test": "vitest run",
"test:types": "vitest run --typecheck",
"build": "tsdown",
"attw": "attw --pack ."
},
"devDependencies": {
"typescript": "^7.0.2",
"oxlint": "^1.93.0",
"@biomejs/biome": "^2.6.7",
"vitest": "^5.1.0",
"tsdown": "^0.23.7",
"@arethetypeswrong/cli": "^0.18.5"
}
}O script check usa --noEmit (verifica sem gerar arquivos) porque quem gera o JavaScript é o tsdown. Se o projeto for uma aplicação com Vite em vez de biblioteca, troque build por vite build e apague o script attw.
Teste de tipos com expectTypeOf
Teste de tipos confere o que o compilador enxerga, sem executar a lógica. Ele é útil quando a biblioteca promete algo na assinatura: por exemplo, que criarLead devolve a origem como união de literais (tipos que aceitam só valores exatos) e que o orçamento sempre chega em centavos como number.
import { describe, expectTypeOf, test } from 'vitest'
import { criarLead, formatarOrcamento, type Lead } from './lead'
describe('tipos do módulo de leads', () => {
test('criarLead devolve um Lead completo', () => {
const lead = criarLead({ email: 'ana@mirante.com.br', origem: 'formulario' })
expectTypeOf(lead).toEqualTypeOf<Lead>()
expectTypeOf(lead.origem).toEqualTypeOf<'formulario' | 'webhook' | 'importacao'>()
})
test('formatarOrcamento recebe centavos e devolve texto', () => {
expectTypeOf(formatarOrcamento).parameter(0).toBeNumber()
expectTypeOf(formatarOrcamento).returns.toBeString()
// @ts-expect-error orçamento em string fica de fora
formatarOrcamento('1500')
})
})O @ts-expect-error (comentário que exige um erro na linha seguinte) inverte a lógica: o teste falha se alguém afrouxar a assinatura e a chamada com string passar a compilar. Arquivos com sufixo .test-d.ts são recolhidos pelo --typecheck e ficam fora da suíte de execução.
A ordem no CI
No CI (integração contínua: o servidor que roda as verificações a cada push), vale começar pelo que é barato e falha cedo. Formatação e lint terminam em segundos. Checagem de tipos vem logo depois, e só então testes e build, que gastam mais tempo.
# 1. instala exatamente o que está no lockfile
npm ci
# 2. formatação e lint sintático, sem reescrever arquivos
npx biome ci .
# 3. tipos do projeto inteiro (tsc 7, sem emitir)
npm run check
# 4. lint por tipos com o motor do TypeScript 7
npm run lint
# 5. testes de tipos e depois testes de execução
npm run test:types
npm test
# 6. empacota e confere se os tipos resolvem para quem instala
npm run build
npm run attwSe o passo 3 quebrar, os passos seguintes nem rodam, e o relatório mostra o erro de tipo sem ruído de testes falhando por consequência. Para bibliotecas publicadas no JSR, o passo 6 pode ser trocado pela publicação no próprio JSR, que verifica o código e gera os .d.ts do lado de lá.
Sete passos de JavaScript até o strict, com a agência rodando
A Mirante tem um painel de campanhas em JavaScript com anos de código e clientes usando todo dia. Migrar tudo de uma vez significa semanas sem entrega. O caminho abaixo troca isso por sete passos pequenos, cada um com valor próprio e reversível.
Passo 1: tsconfig que aceita o que já existe
O primeiro tsconfig.json liga allowJs (o compilador passa a ler arquivos .js) e deixa checkJs desligado (ele lê, mas não reclama dos tipos neles). Atenção: desde o TypeScript 6.0 o strict (conjunto de checagens rígidas, como proibir any implícito) vem ligado por padrão. Por isso, na largada, ele precisa aparecer desligado de forma explícita.
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false, // padrão virou true no 6.0
"noEmit": true, // o bundler continua gerando o JS
"module": "esnext",
"moduleResolution": "bundler",
"types": ["node"] // desde o 6.0, @types não entram sozinhos
},
"include": ["src"]
}Passo 2: tsc no CI desde o primeiro dia
Coloque npx tsc no CI (integração contínua: a verificação automática que roda a cada push). Com checkJs desligado ele quase não acusa nada, e esse é o objetivo: a partir de agora, todo arquivo .ts novo já nasce verificado. No TypeScript 7 o comando continua tsc, só que bem mais rápido.
Passo 3: @ts-check e JSDoc nos arquivos críticos
Antes de renomear qualquer coisa, escolha os arquivos que mais quebram: captura de leads, cálculo de orçamento, webhooks. Um comentário // @ts-check no topo liga a verificação só naquele .js. Os tipos entram via JSDoc (comentários de documentação que o compilador entende): @param para parâmetros, @typedef para declarar um tipo e @import (do 5.5) para trazer tipos de outro arquivo.
// @ts-check
/** @import { Origem } from "./tipos.js" */
/**
* @typedef {Object} Lead
* @property {string} email
* @property {Origem} origem
* @property {number} orcamentoCentavos
*/
/**
* @param {string} email
* @param {Origem} origem
* @param {number} [orcamentoCentavos]
* @returns {Lead}
*/
export function registrarLead(email, origem, orcamentoCentavos = 0) {
return { email: email.trim().toLowerCase(), origem, orcamentoCentavos };
}O equivalente em TypeScript diz o mesmo com menos ruído. Repare no tipo inferido de lead:
type Origem = "formulario" | "webhook" | "importacao";
interface Lead {
email: string;
origem: Origem;
orcamentoCentavos: number;
}
function registrarLead(email: string, origem: Origem, orcamentoCentavos = 0): Lead {
return { email: email.trim().toLowerCase(), origem, orcamentoCentavos };
}
const lead = registrarLead("Ana@Mirante.com.br", "formulario");
// ^?- linha 13
const lead: Lead - resultado
sem erro de tipo, com strict
Passo 4: renomear por pasta e conter os erros
Renomeie uma pasta inteira de .js para .ts por vez, num PR (pull request, o pedido de revisão) só para isso. Os erros que aparecerem e não couberem no PR recebem @ts-expect-error (diretiva que silencia o erro da linha seguinte) com uma explicação.
Prefira essa diretiva ao @ts-ignore. O @ts-ignore silencia para sempre, mesmo depois que o problema some. O @ts-expect-error vira erro (TS2578) quando a linha de baixo para de errar, então a pendência se denuncia sozinha.
function formatarReais(centavos: number): string {
return (centavos / 100).toFixed(2);
}
const daPlanilha: unknown = "15000";
const jaValidado = 15000;
// @ts-expect-error TODO(migracao): validar o valor da planilha
formatarReais(daPlanilha);
// erro esperado: TS2578
// @ts-expect-error sobrou depois da correção
formatarReais(jaValidado);- linha 12
TS2578: Unused '@ts-expect-error' directive.
Passo 5: strict por pasta
Quando uma pasta fica limpa, ela ganha um tsconfig próprio que estende o da raiz e liga o strict. O CI passa a rodar os dois projetos.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true,
"allowJs": false // nesta pasta, só .ts
},
"include": ["."]
}npx tsc -p tsconfig.json
npx tsc -p src/campanhas/tsconfig.jsonRepita pasta por pasta. As de menor risco (componentes de interface, utilitários) costumam ir primeiro.
Passo 6: caçar os @ts-expect-error com contagem no CI
Diretivas acumulam se ninguém olhar. Guarde o total atual num arquivo e faça o CI falhar se o número subir. Quando alguém reduzir, atualiza o arquivo no mesmo PR, e o teto desce.
LIMITE=$(cat .limite-ts-expect-error)
ATUAL=$(grep -rn "@ts-expect-error" src --include="*.ts" --include="*.tsx" | wc -l)
echo "Diretivas: $ATUAL (limite: $LIMITE)"
if [ "$ATUAL" -gt "$LIMITE" ]; then
echo "Mais @ts-expect-error do que o permitido."
exit 1
fiPasso 7: fechar a porta com allowJs falso
Com todas as pastas em strict e o contador perto de zero, a raiz troca allowJs para false e strict para true. Os tsconfigs de pasta podem sumir, porque agora repetem a raiz. A partir daí, um .js novo em src deixa de fazer parte do projeto e o CI acusa o primeiro import que apontar para ele.
| Passo | Mudança | O que a equipe ganha |
|---|---|---|
| 1 | allowJs ligado, checkJs e strict desligados | Projeto compila sem tocar em código |
| 2 | tsc no CI | Arquivos .ts novos já verificados |
| 3 | // @ts-check e JSDoc | Tipos nos pontos críticos, ainda em .js |
| 4 | Renomear por pasta, @ts-expect-error | Pastas inteiras em TypeScript |
| 5 | tsconfig de pasta com strict | Checagem rígida onde já dá |
| 6 | Contagem de diretivas no CI | Pendências que só diminuem |
| 7 | allowJs falso na raiz | Projeto inteiro em TypeScript estrito |
Leia o código do erro e corrija a causa
O diagnóstico (mensagem do compilador) aponta o código do erro, a posição e os tipos envolvidos. Leia primeiro a linha indicada e depois a relação entre o valor recebido e o tipo esperado. Nos exemplos, strict (conjunto de verificações rigorosas) está ligado, e cada comentário erro esperado identifica a única falha intencional do bloco.
TS2322: Type 'string' is not assignable to type 'number'
O valor atribuído tem um tipo incompatível com o declarado, como um texto colocado no orçamento em centavos da Mirante.
// O orçamento é um número em centavos.
// erro esperado: TS2322
const orcamento: number = "15000";
// Correção: use um valor numérico.
const orcamentoCorrigido: number = 15000;- linha 3
TS2322: Type 'string' is not assignable to type 'number'.
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
O argumento (valor passado na chamada) tem um tipo diferente do parâmetro (entrada declarada pela função).
function registrarOrcamento(centavos: number) { return centavos; }
// erro esperado: TS2345
registrarOrcamento("15000");
// Correção: passe o tipo exigido pela função.
registrarOrcamento(15000);- linha 3
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
TS2339: Property 'telefone' does not exist on type '{ email: string; }'
A propriedade (campo de um objeto) acessada está ausente do tipo conhecido pelo compilador.
const lead = { email: "contato@mirante.example" };
// erro esperado: TS2339
lead.telefone;
// Correção: inclua o campo no objeto que será usado.
const leadCompleto = { ...lead, telefone: "11999990000" };
leadCompleto.telefone;- linha 3
TS2339: Property 'telefone' does not exist on type '{ email: string; }'.
TS2304: Cannot find name 'campanhaAtiva'
O identificador (nome usado no código) está sem declaração acessível naquele escopo (trecho em que um nome pode ser usado).
// erro esperado: TS2304
const nome = campanhaAtiva;
// Correção: declare os dados e use o nome disponível.
const configuracao = { campanha: "Mirante" };
const nomeCorrigido = configuracao.campanha;- linha 2
TS2304: Cannot find name 'campanhaAtiva'.
TS2307: Cannot find module '@mirante/contratos-leads' or its corresponding type declarations
O compilador falhou ao localizar o módulo (arquivo ou pacote importado) ou suas declarações de tipos (descrições das estruturas que ele fornece). O pacote fictício abaixo está indisponível, por isso o exemplo fica fora do laboratório. Em uma integração real, confira o nome, a instalação e o caminho; para este exemplo isolado, substitua a importação pelo tipo local indicado.
// erro esperado: TS2307
import type { LeadRemoto } from "@mirante/contratos-leads";
// Correção local: remova a importação e declare o contrato usado.
type Lead = { email: string };
const lead: Lead = { email: "contato@mirante.example" };TS7006: Parameter 'email' implicitly has an 'any' type
O parâmetro ficou com any implícito (tipo que permite operações sem verificá-las) porque o compilador não conseguiu deduzir seu tipo.
// erro esperado: TS7006
const normalizar = (email) => email.trim();
// Correção: declare o tipo da entrada.
const normalizarEmail = (email: string) => email.trim();
normalizarEmail(" contato@mirante.example ");- linha 2
TS7006: Parameter 'email' implicitly has an 'any' type.
TS2532: Object is possibly 'undefined'
O acesso usa um objeto que pode ser undefined (valor que representa ausência), como o resultado de uma busca sem lead encontrado.
function buscarLead(): { email: string } | undefined { return undefined; }
// erro esperado: TS2532
const email = buscarLead().email;
// Correção: ?. acessa apenas se houver objeto; ?? fornece o padrão.
const emailCorrigido = buscarLead()?.email ?? "";- linha 3
TS2532: Object is possibly 'undefined'.
TS18048: 'lead' is possibly 'undefined'
O nome indicado pode conter undefined, e o compilador exige tratar essa ausência antes de acessar seus campos.
function lerEmail(lead?: { email: string }) {
// erro esperado: TS18048
const email = lead.email;
// Correção: trate o parâmetro opcional.
return lead?.email ?? "";
}- linha 3
TS18048: 'lead' is possibly 'undefined'.
TS2769: No overload matches this call
Nenhuma sobrecarga (assinatura de chamada aceita por uma função) corresponde aos argumentos fornecidos.
function formatarMetrica(valor: string): string;
function formatarMetrica(valor: number): string;
function formatarMetrica(valor: string | number): string {
return String(valor);
}
// erro esperado: TS2769
formatarMetrica(true);
// Correção: use uma das entradas declaradas.
formatarMetrica(120);- linha 7
TS2769: No overload matches this call. Overload 1 of 2, '(valor: string): string', gave the following error. Argument of type 'boolean' is not assignable to param
TS2741: Property 'email' is missing in type '{}' but required in type 'Lead'
O objeto recebido está sem uma propriedade obrigatória do tipo esperado.
type Lead = { email: string };
// erro esperado: TS2741
const lead: Lead = {};
// Correção: forneça o campo obrigatório.
const leadCorrigido: Lead = { email: "contato@mirante.example" };- linha 3
TS2741: Property 'email' is missing in type '{}' but required in type 'Lead'.
TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ cliques: number; leads: number; }'
Uma chave declarada como string permite qualquer texto, mas o objeto aceita apenas campos conhecidos, que podem ser representados por keyof (operador que extrai as chaves de um tipo).
const painel = { cliques: 120, leads: 8 };
const metrica: string = "leads";
// erro esperado: TS7053
const total = painel[metrica];
// Correção: typeof obtém o tipo do objeto; keyof limita as chaves.
const chave: keyof typeof painel = "leads";
const totalCorrigido = painel[chave];- linha 4
TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ cliques: number; leads: number; }'. No index signature
TS2353: Object literal may only specify known properties, and 'telefone' does not exist in type 'Lead'
Um objeto literal (objeto escrito diretamente com chaves) contém um campo extra que o tipo de destino não declara.
type Lead = { email: string };
// erro esperado: TS2353
const lead: Lead = { email: "contato@mirante.example", telefone: "11999990000" };
// Correção: envie apenas os campos previstos no contrato.
const leadCorrigido: Lead = { email: "contato@mirante.example" };- linha 3
TS2353: Object literal may only specify known properties, and 'telefone' does not exist in type 'Lead'.
TS2589: Type instantiation is excessively deep and possibly infinite
A avaliação de um tipo recursivo (tipo que referencia a si mesmo) ultrapassou o limite de trabalho do compilador, como nesta repetição sem condição de parada.
type Repetir<T> = T extends unknown ? Repetir<T> : never;
// erro esperado: TS2589
type Leads = Repetir<{ email: string }>;
// Correção: represente a coleção sem a repetição recursiva.
type ListaSegura<T> = T[];
type LeadsCorrigidos = ListaSegura<{ email: string }>;- linha 3
TS2589: Type instantiation is excessively deep and possibly infinite.
TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled
A sintaxe usada exige transformação em JavaScript, enquanto erasableSyntaxOnly permite apenas construções TypeScript que possam ser apagadas. No laboratório, a linha // @erasableSyntaxOnly: true no topo do exemplo liga a opção sozinha. A correção usa um objeto com as const (marcação que preserva os valores específicos e torna suas propriedades somente leitura na checagem de tipos).
// @erasableSyntaxOnly: true
// erro esperado: TS1294
enum Etapa { Novo, Qualificado }
// Correção: use um objeto JavaScript com valores específicos.
const etapas = { Novo: "novo", Qualificado: "qualificado" } as const;- linha 3
TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
Laboratório: o compilador TypeScript de verdade, aqui na página
Simulação ensina até certo ponto; depois disso só o compilador responde. O laboratório baixa o TypeScript 6.0.3 (o último escrito em JavaScript, o que o deixa rodar no navegador; o 7.0 é um programa nativo em Go) e as bibliotecas de tipos do JavaScript moderno, só quando você pede. São cerca de 9 MB na primeira vez, guardados pelo navegador para o resto da visita. Depois disso cada verificação leva milissegundos, com strict ligado.
Os 101 blocos com faixa azul na página têm o botão “Abrir no laboratório”. Lá você edita, aperta Ctrl+Enter e vê os erros com código, linha e mensagem (um clique leva à linha), o tipo de qualquer nome com “Tipo no cursor”, o resultado de cada marcador ^?, o JavaScript que sai e o .d.ts que uma biblioteca publicaria. As caixas ligam quatro opções de rigor, entre elas erasableSyntaxOnly (a regra do Node sem build) e isolatedDeclarations.
O laboratório abre por cima da leitura; Esc ou “Fechar” devolvem você ao mesmo ponto.
Catálogo: 60 padrões do sistema de tipos, cada um com um exemplo que compila
Uma consulta rápida para quando o nome do recurso escapa: oito famílias, da base (unknown, never, satisfies) aos padrões de produto (marca de tipo, máquina de estados, using). Cada linha traz o que o padrão resolve, um exemplo de uma linha e a armadilha mais comum. Filtre pela família ou procure por palavra, e o botão “Abrir” leva o exemplo ao laboratório, onde dá para mexer e ver o compilador reagir.
60 de 60 padrões
| Padrão | O que resolve | Exemplo | Armadilha | Laboratório |
|---|---|---|---|---|
| Tipos primitivos | Representam categorias básicas de valores sem recorrer a objetos encapsuladores. | let nome: string = "Ana"; let idade: number = 30; let ativo: boolean = true; | Prefira string, number e boolean a String, Number e Boolean. | |
| Tipos literais | Restringem um valor a uma constante específica. | type Direcao = "norte"; const direcao: Direcao = "norte"; | Uma variável mutável pode ter seu literal ampliado para string ou number durante a inferência. | |
| `null` e `undefined` | Modelam explicitamente ausência de valor. | let ausencia: null | undefined = undefined; | Sem strictNullChecks, perde-se boa parte da proteção contra valores ausentes. | |
| `any` | Permite uma saída explícita da verificação estática em pontos de interoperabilidade. | const dado: any = "texto"; const tamanho: number = dado.length; | any pode propagar-se e esconder incompatibilidades em operações posteriores. | |
| `unknown` | Recebe valores arbitrários exigindo verificação antes do uso específico. | const dado: unknown = "oi"; if (typeof dado === "string") dado.toUpperCase(); | Uma asserção com as elimina a exigência de estreitamento sem validar o valor. | |
| `never` | Representa valores impossíveis e funções que nunca retornam normalmente. | function falhar(mensagem: string): never { throw new Error(mensagem); } | Uma função que apenas termina sem retornar valor tem retorno void, não never. | |
| `void` | Expressa que o consumidor não deve utilizar o resultado de uma função. | const executar: () => void = () => 123; | Uma função atribuída a () => void pode retornar um valor em runtime. | |
| `unique symbol` | Cria identidades de tipo exclusivas para chaves e marcadores. | const chave: unique symbol = Symbol("chave"); type Registro = { [chave]: string }; | Dois símbolos criados com a mesma descrição continuam tendo identidades distintas. | |
| `as const` | Preserva literais e infere propriedades somente leitura e tuplas readonly em expressões literais. | const config = { modo: "dev", portas: [80, 443] } as const; | A asserção não congela objetos em runtime nem torna imutáveis objetos externos referenciados. | |
| `satisfies` | Verifica compatibilidade com um contrato sem substituir o tipo da expressão por esse contrato. | const config = { modo: "dev" } satisfies { modo: "dev" | "prod" }; | Não valida dados em runtime e seu contexto de tipagem pode influenciar a inferência. | |
| Aliases com `type` | Dão nomes reutilizáveis a expressões de tipo. | type Ponto = { x: number; y: number }; | Aliases não participam de declaração mesclada como interfaces. | |
| Interfaces e declaration merging | Permitem declarar contratos de objetos extensíveis por novas declarações. | interface Pessoa { nome: string } interface Pessoa { idade: number } | Propriedades repetidas com tipos incompatíveis causam erro em vez de formar uma união. | |
| Propriedades opcionais | Representam propriedades que podem estar ausentes de um objeto. | type Opcoes = { limite?: number }; const opcoes: Opcoes = {}; | Com exactOptionalPropertyTypes, ausência não equivale a atribuir explicitamente undefined. | |
| Propriedades `readonly` | Impedem reatribuição de propriedades através de uma referência tipada. | type Usuario = { readonly id: number }; const usuario: Usuario = { id: 1 }; | A restrição é estática e superficial, não uma garantia de imutabilidade em runtime. | |
| Assinaturas de índice | Descrevem objetos com chaves dinâmicas e valores uniformes. | type Contagens = { [chave: string]: number }; const contagens: Contagens = { visitas: 3 }; | Sem noUncheckedIndexedAccess, uma leitura de chave inexistente pode parecer retornar um valor presente. | |
| Tuplas | Representam sequências cujas posições têm tipos e papéis específicos. | type Linha = [id: number, rotulo?: string, ...tags: string[]]; | Os rótulos documentam posições, mas não criam propriedades acessíveis por esses nomes. | |
| Interseções | Combinam requisitos de múltiplos tipos em um único valor. | type Entidade = { id: string } & { criadoEm: Date }; | Propriedades conflitantes podem resultar em never, não em uma escolha entre os tipos. | |
| Reflexão com `keyof typeof` | Extrai uma união de chaves a partir do tipo de um valor existente. | const cores = { azul: "#00f", vermelho: "#f00" }; type Cor = keyof typeof cores; | A união descreve chaves conhecidas estaticamente e não transforma Object.keys em uma lista tipada delas. | |
| Uniões | Permitem que um valor pertença a qualquer uma de várias alternativas. | type Identificador = string | number; const id: Identificador = 42; | Operações específicas de uma alternativa exigem estreitamento prévio. | |
| Guarda com `typeof` | Estreita valores conforme sua categoria primitiva em runtime. | function tamanho(x: string | number): number { return typeof x === "string" ? x.length : x; } | typeof null retorna "object" e não distingue objetos comuns de arrays. | |
| Guarda com `in` | Estreita alternativas pela presença de uma propriedade. | function ler(x: { a: number } | { b: string }) { return "a" in x ? x.a : x.b; } | Uma propriedade opcional pode manter a mesma alternativa possível nos dois ramos. | |
| Guarda com `instanceof` | Estreita valores por sua relação com um construtor em runtime. | function ano(x: Date | string) { return x instanceof Date ? x.getFullYear() : Number(x); } | Interfaces desaparecem na compilação e não podem ser usadas como construtores no teste. | |
| Uniões discriminadas | Relacionam uma etiqueta literal aos campos válidos de cada alternativa. | type Resultado = { ok: true; valor: number } | { ok: false; erro: string }; | Usar boolean em ambas as alternativas, em vez de literais opostos, perde a discriminação. | |
| Predicados de tipo | Encapsulam verificações reutilizáveis que estreitam argumentos. | function ehTexto(x: unknown): x is string { return typeof x === "string"; } | O compilador confia no predicado declarado e não comprova que sua implementação é correta. | |
| Assertion functions | Estabelecem uma condição de tipo após uma função retornar normalmente. | function exigirTexto(x: unknown): asserts x is string { if (typeof x !== "string") throw new Error("Texto esperado"); } | Retornar normalmente quando a condição é falsa torna o contrato de asserção incorreto. | |
| Exaustividade com `never` | Faz novas alternativas não tratadas aparecerem como erros de compilação. | type S = "a" | "b"; function f(s: S): number { switch (s) { case "a": return 1; case "b": return 2; default: { const impossivel: never = s; return impossivel; } } } | Um default que retorna um valor genérico sem testar never pode esconder casos esquecidos. | |
| Funções genéricas | Preservam relações entre tipos de entrada e saída. | function identidade<T>(valor: T): T { return valor; } | Substituir T por any remove a relação verificável entre argumento e retorno. | |
| Restrições com `extends` | Limitam argumentos de tipo aos contratos necessários para uma operação. | function tamanho<T extends { length: number }>(valor: T): number { return valor.length; } | A restrição garante apenas os membros declarados, não um subtipo concreto como string. | |
| Parâmetros de tipo padrão | Fornecem uma escolha de tipo quando não há argumento explícito ou inferência aplicável. | type Caixa<T = string> = { valor: T }; const caixa: Caixa = { valor: "oi" }; | Um parâmetro de tipo obrigatório não pode vir depois de outro com padrão. | |
| Interfaces genéricas | Parametrizam contratos inteiros mantendo consistência entre seus membros. | interface Repositorio<T> { obter(id: string): T | undefined; salvar(valor: T): void } | Usar Repositorio<any> enfraquece tanto as entradas quanto as saídas do contrato. | |
| Acesso indexado dependente | Relaciona o tipo de retorno à chave efetivamente solicitada. | function obter<T, K extends keyof T>(obj: T, chave: K): T[K] { return obj[chave]; } | Trocar o retorno por T[keyof T] perde a precisão da chave específica. | |
| Parâmetros de tipo `const` | Favorecem inferência literal e de tuplas para expressões fornecidas na chamada. | function tupla<const T extends readonly unknown[]>(x: T): T { return x; } const t = tupla(["a", 1]); | Não recuperam literais já ampliados em variáveis declaradas anteriormente. | |
| `NoInfer` | Impede que uma posição forneça candidatos para inferir um parâmetro de tipo. | function escolher<T>(opcoes: readonly T[], padrao: NoInfer<T>): T { return opcoes[0] ?? padrao; } escolher(["a", "b"] as const, "a"); | Bloquear inferência não elimina a verificação de compatibilidade do argumento. | |
| Sobrecargas de funções | Expõem assinaturas diferentes para combinações específicas de argumentos. | function texto(x: string): string; function texto(x: number): string; function texto(x: string | number): string { return String(x); } | A assinatura da implementação não constitui uma sobrecarga pública adicional. | |
| `Partial<T>` | Torna opcionais todas as propriedades imediatas de um tipo. | type Patch = Partial<{ nome: string; idade: number }>; | A transformação não torna opcionais as propriedades de objetos aninhados. | |
| `Required<T>` | Remove a opcionalidade das propriedades imediatas de um tipo. | type Completo = Required<{ nome?: string; idade?: number }>; | Não cria valores ausentes em runtime nem normaliza objetos recebidos. | |
| `Readonly<T>` | Produz uma visão superficial sem reatribuição de propriedades. | type Config = Readonly<{ porta: number; tags: string[] }>; | O array tags continua mutável, embora a propriedade não possa ser reatribuída. | |
| `Pick<T, K>` | Seleciona um subconjunto explícito de propriedades. | type Resumo = Pick<{ id: number; nome: string; senha: string }, "id" | "nome">; | Selecionar propriedades no tipo não remove campos do objeto em runtime. | |
| `Omit<T, K>` | Produz um tipo sem determinadas propriedades. | type Publico = Omit<{ nome: string; senha: string }, "senha">; | O utilitário padrão não preserva automaticamente cada alternativa de uma união como um Omit distributivo. | |
| `Record<K, V>` | Define um conjunto de chaves cujos valores seguem o mesmo tipo. | type Placar = Record<"azul" | "vermelho", number>; const placar: Placar = { azul: 0, vermelho: 0 }; | Record<string, V> não garante que toda chave consultada exista em runtime. | |
| `ReturnType<F>` | Extrai o retorno de um tipo de função para evitar duplicação de contratos. | function criar() { return { id: 1 }; } type Criado = ReturnType<typeof criar>; | Para funções sobrecarregadas, a extração considera a última assinatura disponível. | |
| `Awaited<T>` | Modela o desembrulhamento recursivo realizado por await. | type Valor = Awaited<Promise<Promise<number>>>; | O utilitário transforma tipos, mas não executa nem aguarda promises. | |
| Tipos condicionais | Escolhem um tipo de saída com base em uma relação de atribuibilidade. | type EhTexto<T> = T extends string ? true : false; | extends testa atribuibilidade, não igualdade exata entre tipos. | |
| Captura com `infer` | Extrai partes de um tipo reconhecido por um padrão condicional. | type Elemento<T> = T extends readonly (infer U)[] ? U : never; | A variável inferida só fica disponível no ramo verdadeiro daquele condicional. | |
| Condicionais distributivos | Aplicam uma transformação separadamente a cada membro de uma união. | type EmLista<T> = T extends unknown ? T[] : never; type R = EmLista<string | number>; | R é string[] | number[], não um array que mistura os dois tipos. | |
| Supressão de distributividade | Faz um condicional avaliar uma união como um conjunto único. | type EmLista<T> = [T] extends [unknown] ? T[] : never; type R = EmLista<string | number>; | Envolver o parâmetro em tupla também altera comportamentos envolvendo never. | |
| Tipos mapeados | Transformam sistematicamente os valores associados às propriedades de um tipo. | type Promessas<T> = { [K in keyof T]: Promise<T[K]> }; | Modificadores como opcionalidade e readonly podem ser preservados quando não são alterados explicitamente. | |
| Modificadores de mapeamento | Adicionam ou removem opcionalidade e restrições de escrita. | type Editavel<T> = { -readonly [K in keyof T]-?: T[K] }; | Remover modificadores no primeiro nível não transforma recursivamente os objetos internos. | |
| Remapeamento de chaves | Renomeia ou filtra propriedades durante um mapeamento. | type SemId<T> = { [K in keyof T as Exclude<K, "id">]: T[K] }; | Diferentes chaves remapeadas para o mesmo nome podem combinar seus tipos de valor. | |
| Condicionais recursivos | Processam estruturas aninhadas até alcançar um caso terminal. | type Desembrulhar<T> = T extends ReadonlyArray<infer U> ? Desembrulhar<U> : T; | Recursão profunda ou expansiva pode atingir limites de instanciação do compilador. | |
| Combinação de literais | Gera conjuntos de strings a partir de combinações de partes conhecidas. | type Evento = `${"click" | "focus"}:${"in" | "out"}`; | Uniões grandes em várias posições podem produzir uma explosão combinatória. | |
| Transformações intrínsecas de caixa | Derivam convenções de nomes com operações como Capitalize e Uppercase. | type Getter = `get${Capitalize<"nome" | "idade">}`; | Essas operações de tipo não alteram strings em runtime nem implementam regras de localização. | |
| Extração de partes de strings | Reconhece formatos literais e captura seus componentes no sistema de tipos. | type Parametro<S extends string> = S extends `:${infer Nome}` ? Nome : never; | Um string genérico não garante o formato e pode produzir never nesse condicional. | |
| Assinaturas de índice por padrão | Restringem chaves dinâmicas a um formato textual específico. | type Atributos = { [chave: `data-${string}`]: string }; | O padrão não exige que exista alguma chave correspondente nem valida objetos em runtime. | |
| Branded types | Distinguem semanticamente valores que compartilham a mesma representação estrutural. | declare const marca: unique symbol; type UsuarioId = string & { readonly [marca]: "UsuarioId" }; | Uma asserção arbitrária pode fabricar a marca sem verificar a regra de domínio. | |
| Builders tipados por etapas | Disponibilizam operações apenas depois que seus pré-requisitos foram preenchidos. | const usuario = () => ({ nome: (nome: string) => ({ criar: () => ({ nome }) }) }); const u = usuario().nome("Ana").criar(); | Expor criar na etapa inicial permite contornar a sequência exigida pelo contrato. | |
| Máquina de estados tipada | Restringe operações aos estados em que são válidas e explicita o estado resultante. | type Parado = { estado: "parado" }; type Rodando = { estado: "rodando" }; function iniciar(_: Parado): Rodando { return { estado: "rodando" }; } | A transição não invalida referências antigas, pois TypeScript não impõe consumo linear de valores. | |
| Parse, don’t validate | Converte uma entrada desconhecida em um valor cujo tipo registra a validação bem-sucedida. | type Id = number & { readonly __tipo: "Id" }; function parseId(x: unknown): Id { if (typeof x !== "number" || !Number.isSafeInteger(x) || x <= 0) throw new Error("Id inválido"); return x as Id; } | Retornar apenas um booleano não entrega ao restante do programa o valor refinado pelo parser. | |
| `using` e `Disposable` | Associam recursos a um contrato de descarte automático na saída do escopo. | { using recurso: Disposable = { [Symbol.dispose]() {} }; } | É necessário disponibilizar os tipos de esnext.disposable e suporte ou polyfill adequado no runtime. | |
| Decorators padrão | Tipam interceptações de classes e membros por meio de funções e objetos de contexto. | function selada(alvo: Function, _ctx: ClassDecoratorContext): void { Object.seal(alvo); } @selada class Servico {} | O modo legado experimentalDecorators usa outro contrato, e decorar não acrescenta automaticamente membros ao tipo público. |
Oficina: vai rodar com node arquivo.ts?
O Node.js roda TypeScript apagando os tipos e executando o que sobra. Isso funciona para quase tudo, menos para a sintaxe que produz código: enum, namespace com valores, propriedades declaradas no construtor e import = require. Cole um arquivo, veja cada trecho que bloqueia com a linha e o motivo, e leve a versão reescrita ao compilador real com erasableSyntaxOnly ligado.
Cenário de exercício
- Quem
- Rafa, desenvolvedor da Mirante, quer tirar o build da API de captura de leads e rodar direto com
node captura.tsno Node 26. - Tarefa
- Descobrir o que impede o arquivo de rodar sem transpilar e reescrever só o necessário.
- Falha vista
- O Node para na primeira linha com
enum: ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. Depois, ERR_MODULE_NOT_FOUND num import sem extensão. - Resultado esperado
- Arquivo só com sintaxe apagável, imports com extensão
.ts,import typepara tipos eerasableSyntaxOnlyno tsconfig para o editor avisar antes.
Analisar: cole o arquivo
Analisando…
| Linha | Trecho | Por quê | Troque por |
|---|
Versão reescrita (só sintaxe apagável)
// O JavaScript monta a reescrita a partir do arquivo acima.A análise desta oficina é uma leitura por padrões de texto, feita para ensinar. Quem decide é o compilador: no laboratório, erasableSyntaxOnly marca cada trecho com o erro TS1294.
Investigar as falhas
| Sintoma | Causa provável | Como confirmar | Correção |
|---|---|---|---|
| ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | enum, namespace com valor ou propriedade no construtor | Ligue erasableSyntaxOnly: o editor marca TS1294 | Objeto as const, módulo comum, campo declarado na classe |
| ERR_MODULE_NOT_FOUND | Import relativo sem extensão | O caminho no erro termina sem .ts | ./lead.ts e allowImportingTsExtensions |
| “does not provide an export named” | Tipo importado como valor | O nome importado é type ou interface | import type, com verbatimModuleSyntax ligado |
SyntaxError no @ | Decorator: o V8 ainda não executa | A linha do erro começa com @ | Transpile esse arquivo (tsc, tsx ou bundler) |
O alias @/ não resolve | O Node não lê paths do tsconfig | Funciona no editor e falha no terminal | imports do package.json (#/) |
Critérios de aceite
Oficina: monte o tsconfig do seu projeto
Escolha o cenário (aplicação Vite, Next, API Node sem build, biblioteca, pacote de monorepo ou script), ligue as chaves de rigor que o time aguenta e marque se ainda há JavaScript no projeto. O arquivo sai pronto, com uma linha de explicação por chave.
Cenário de exercício
- Quem
- Bia, que cuida do painel de campanhas da Mirante, herdou três projetos com tsconfig copiado de 2021.
- Tarefa
- Atualizar os três para o TypeScript 6.0 e 7.0 sem avisos de descontinuação.
- Falha vista
baseUrl,moduleResolution: nodeetarget: es5geram avisos no 6.0 e erro no 7.0.- Resultado esperado
- Um tsconfig curto por projeto, em que cada linha tem um motivo e nada depende de opção removida.
Montar
Montando…
{
"compilerOptions": { "strict": true }
}O que cada chave faz
Critérios de aceite
Oficina: o formulário de lead passa pelo esquema
O navegador entrega o formulário como texto: FormData não sabe o que é número, a caixa desmarcada simplesmente some e o select aceita qualquer valor que alguém forjar. O esquema é a porta: ele recebe unknown e devolve um Lead ou uma lista de problemas com caminho e código. Preencha, envie, ligue a coerção do orçamento e veja o tipo mudar junto com o esquema.
Cenário de exercício
- Quem
- Lu, da Mirante, cuida do formulário de orçamento da Clínica Sorriso.
- Tarefa
- Garantir que o CRM só receba lead completo: nome, e-mail válido, orçamento em centavos e aceite dos termos.
- Falha vista
- Leads chegando com orçamento
"4.500"(texto), canal"tiktok"que não existe na lista e aceite em branco. - Resultado esperado
- Um único esquema Zod que valida no navegador e no servidor, com o tipo
Leadderivado dele porz.infer.
Preencher e enviar
Preencha o formulário.
{}A validação desta oficina reproduz em JavaScript puro as regras do esquema e os códigos de problema do Zod 4 (too_small, invalid_format, invalid_type, invalid_value), para a página não carregar a biblioteca. No projeto, o mesmo arquivo lead.schema.ts roda no navegador e no servidor.
Critérios de aceite
Oficina: o botão de envio como máquina de estados
Um botão de envio tem quatro estados que importam (ocioso, enviando, enviado e erro) e cada um carrega dados diferentes: o protocolo só existe depois do sucesso, o motivo só existe no erro. Uma união discriminada escreve isso como tipo, e o switch com never no fim faz o compilador apontar o estado esquecido. Clique no botão, force uma falha, mande eventos fora de hora e depois apague um case para ver o erro que o compilador daria.
Cenário de exercício
- Quem
- Caio, que mantém o botão “Enviar inscrição” da landing page de um evento.
- Tarefa
- Acabar com o envio duplo e com a mensagem “undefined” que aparecia depois de uma falha.
- Falha vista
- O estado era um objeto com
carregando,erroeprotocolosoltos: dava para estar carregando e com erro ao mesmo tempo. - Resultado esperado
- Um tipo
Envioem que os estados impossíveis não cabem, e um redutor que o compilador confere caso a caso.
Clicar no botão
const estado: Envio = { status: "ocioso" };Quebrar o código de propósito
Critérios de aceite
Oficina: multiplique nomes com template literal types
Um template literal type (tipo de texto montado com ${}, como uma template string do JavaScript) cruza uniões: três tamanhos vezes quatro variantes dão doze classes de botão válidas, e qualquer outra grafia vira erro. A conta cresce depressa. Monte o modelo, ponha as listas, aplique Capitalize e veja quantas combinações saem, até passar das 100.000 que o compilador aceita.
Cenário de exercício
- Quem
- Nina, que cuida do sistema de design da Mirante (botões, tokens de cor, eventos de análise).
- Tarefa
- Tipar as classes de botão, os nomes de evento do GA4 e os tokens CSS sem escrever a lista na mão.
- Falha vista
- Classe
btn-primaio-mdcom erro de digitação em produção; eventoonLeadcriadoque o painel nunca contou. - Resultado esperado
- Tipos gerados por combinação, com contagem conhecida e longe do limite.
Montar
Montando…
Critérios de aceite
O agente escreve, o compilador confere
Tipos como contrato para o agente
Um agente de código (programa que usa um LLM, modelo de linguagem grande, para ler arquivos, editar código e rodar comandos) erra com confiança. Ele inventa um campo, troca um nome ou devolve o valor no formato errado. Os tipos do projeto funcionam como contrato escrito: o agente lê as interfaces antes de editar, e o compilador recusa o que foge delas. Na Mirante, o estado de um lead fica descrito numa união de strings, e qualquer valor inventado vira erro na hora.
type EstadoLead = "novo" | "qualificado" | "perdido";
interface Lead {
email: string;
estado: EstadoLead;
orcamentoCentavos: number;
}
function qualificar(lead: Lead): Lead {
return { ...lead, estado: "qualificado" };
}
// o agente "inventou" um estado que não existe
const lead: Lead = {
email: "ana@mirante.com.br",
// erro esperado: TS2322
estado: "quente",
orcamentoCentavos: 150000,
};
qualificar(lead);- linha 17
TS2322: Type '"quente"' is not assignable to type 'EstadoLead'.
O tsc no laço do agente
O laço do agente é o ciclo de editar, rodar um comando, ler a saída e corrigir. Coloque o compilador nesse ciclo como revisor automático: depois de cada mudança, o agente roda a verificação e só para quando ela passa. Com o TypeScript 7 (compilador nativo em Go), a checagem completa fica rápida o bastante para rodar a cada edição.
# verifica tipos sem gerar arquivos
npx tsc --noEmit
# testes de tipos com Vitest
npx vitest run --typecheck
# lint que entende tipos
npx oxlint --type-awareEscreva essas regras no arquivo de instruções do agente: "rode npx tsc --noEmit antes de dizer que terminou" e "não use any nem @ts-ignore para silenciar erro".
Saída estruturada validada com Zod
Saída estruturada é quando você pede ao modelo um JSON num formato combinado. O modelo pode obedecer quase sempre, e esse quase é o problema. Trate a resposta como unknown (tipo que obriga a verificar antes de usar) e passe por um esquema (descrição do formato que valida dados em tempo de execução). O Zod converte o JSON e entrega o tipo inferido com z.infer, sem declarar nada duas vezes.
import { z } from "zod";
const ResumoCampanha = z.object({
campanha: z.string(),
leads: z.number().int().nonnegative(),
orcamentoCentavos: z.number().int(),
status: z.enum(["ativa", "pausada", "encerrada"]),
contato: z.email(),
});
type ResumoCampanha = z.infer<typeof ResumoCampanha>;
export function lerResposta(texto: string): ResumoCampanha {
const bruto: unknown = JSON.parse(texto);
const resultado = ResumoCampanha.safeParse(bruto);
if (!resultado.success) {
// devolva o erro ao modelo e peça outra tentativa
throw new Error(z.prettifyError(resultado.error));
}
return resultado.data;
}Valibot e ArkType fazem o mesmo papel. Como os três seguem o Standard Schema (interface comum entre bibliotecas de validação), um SDK que aceita esse padrão recebe qualquer um deles sem adaptador.
Ferramentas com esquema tipado
Ferramenta (tool) é uma função que o modelo pode pedir para executar: criar lead, buscar métricas, pausar campanha. O modelo manda os argumentos em JSON, então a entrada precisa de esquema. Um auxiliar genérico liga o esquema à função e faz os argumentos chegarem tipados.
import { z } from "zod";
function definirFerramenta<S extends z.ZodType, R>(f: {
nome: string;
descricao: string;
entrada: S;
executar: (args: z.infer<S>) => Promise<R>;
}) {
return f;
}
export const criarLead = definirFerramenta({
nome: "criar_lead",
descricao: "Cadastra um lead vindo de formulário da Mirante",
entrada: z.object({
nome: z.string().min(2),
email: z.email(),
campanhaId: z.uuid(),
}),
executar: async ({ nome, email, campanhaId }) => {
// os três campos já chegam tipados
return { id: crypto.randomUUID(), nome, email, campanhaId };
},
});
// valida os argumentos do modelo antes de executar
export async function rodar(argsDoModelo: unknown) {
const args = criarLead.entrada.parse(argsDoModelo);
return criarLead.executar(args);
}O mesmo esquema gera o JSON Schema que você envia ao modelo (z.toJSONSchema(criarLead.entrada)). Assim, a descrição enviada e a validação aplicada nunca se desencontram.
Sete prompts prontos
- Tipar um formulário de lead:
Tipe o formulário em src/form-lead.tsx com um esquema Zod e z.infer.
Orçamento em centavos (inteiro). Sem any. Rode npx tsc --noEmit no fim.- Migrar um arquivo JS:
Converta src/webhook.js para TypeScript sem mudar o comportamento.
Use unknown nas entradas externas e mostre os erros do tsc que corrigiu.- Criar união discriminada de estados:
Modele o estado do painel de campanha como união discriminada pelo
campo "status": carregando, erro (com mensagem) e pronto (com dados).
Use um switch exaustivo com never no default.- Escrever teste de tipos:
Escreva testes de tipos com expectTypeOf do Vitest para definirFerramenta.
Inclua um caso que deve falhar, marcado com @ts-expect-error.- Endurecer o tsconfig:
Proponha mudanças no tsconfig.json: noUncheckedIndexedAccess e
exactOptionalPropertyTypes. Liste os erros novos e corrija um por vez.- Validar webhook:
Crie a rota do webhook de leads validando o corpo com Zod (safeParse).
Responda 400 com o erro formatado quando o corpo for inválido.- Revisar
any:
Encontre todo any explícito e implícito em src/. Para cada um, troque
por um tipo real ou unknown com verificação. Não use @ts-ignore.Antes do deploy: doze conferências e três caminhos para começar
Antes de publicar: conferência em 12 itens
Use a lista no pull request (pedido de revisão de código) de qualquer projeto da Mirante, seja um formulário de captura, seja uma biblioteca interna de componentes. Os itens 5, 6 e 12 só valem em cenários específicos; os demais valem sempre.
tsc --noEmitsem erro no CI (integração contínua: o servidor que roda as verificações a cada envio de código). O--noEmitcheca os tipos sem gerar arquivos.strictligado. No TypeScript 6.0 ele já vem ligado por padrão. Confira se ninguém desligou notsconfig.jsonpara calar um erro.- Nenhum
anynovo. Oanydesliga a checagem daquele valor. Para dado de tipo desconhecido, useunknown(obriga a verificar antes de usar). - Dado externo validado na borda. A borda é o ponto onde o dado entra: payload de webhook, resposta de API, corpo do formulário. Passe tudo por um schema (descrição do formato esperado) em Zod, Valibot ou ArkType antes de confiar no tipo.
erasableSyntaxOnlyligado se o código roda no Node sem build. O Node faz type stripping (apaga os tipos e executa) e rejeitaenum,namespacecom código e parameter properties (constructor(private x)). A opção acusa essa sintaxe já no editor.- Imports relativos com
.tseimport typepara tipos, no mesmo cenário do item 5. O Node também não lê otsconfig.json, então ajustes de caminho feitos lá não valem na execução. - Tipos e funções exportados com retorno explícito. Assim uma mudança interna não altera o contrato público sem aviso. A opção
isolatedDeclarationscobra isso em todo o projeto. - Nenhum aviso de descontinuação adiado sem prazo.
"ignoreDeprecations": "6.0"serve para a transição.baseUrl,outFileemoduleResolution: "node10"foram removidos no 7.0. noUncheckedSideEffectImportsligado (padrão no 6.0). Um import só por efeito colateral, comoimport "./tema.css", precisa apontar para um arquivo que existe.- Lint por tipos rodando: typescript-eslint ou Oxlint com
--type-aware. Ele pega promessas esquecidas semawaite condições sempre verdadeiras. - Testes de tipo nas APIs públicas. O Vitest tem
expectTypeOfe o modo--typecheckpara garantir quecriarCampanha()continua devolvendo o tipo certo. attwverde se o projeto é uma biblioteca. O Are the Types Wrong confere se os.d.ts(arquivos só com tipos) batem com osexportsdopackage.jsonem cada modo de resolução.
Os itens automatizáveis cabem num único script de CI:
# 1. verificação de tipos, sem gerar arquivos
npx tsc --noEmit
# 10. lint com informação de tipos
npx oxlint --type-aware
# 11. testes, incluindo os testes de tipo
npx vitest run --typecheck
# 12. só para bibliotecas: empacota e confere os tipos publicados
npx attw --pack .Por onde começar hoje
Escolha a trilha que descreve o seu momento. Cada passo cabe numa tarde de trabalho.
Trilha 1: quem nunca usou TypeScript
- Abra o laboratório e escreva um tipo
Leadcom nome, email e origem. Erre de propósito e leia a mensagem do compilador. - Leia o básico: tipos primitivos, objetos, funções e inferência (o compilador deduz o tipo sozinho).
- Siga para uniões e modele o status de um lead (
"novo" | "qualificado" | "perdido"). - Crie um projeto pequeno com o tsconfig recomendado e converta um formulário de captura que já existe em JavaScript.
Trilha 2: quem já usa, mas com any espalhado
- Conte os
anydo projeto e ligue a regrano-explicit-anycomo aviso. O número vira meta de redução no painel do time. - Comece pela borda: troque o
anydos webhooks e das respostas de API porunknownmais um schema, como mostra validação. - Substitua objetos cheios de campos opcionais por uniões discriminadas (cada estado com um campo fixo que o identifica), revisando uniões.
- Siga o roteiro de migração: ative as checagens pasta por pasta, começando pelo código que mexe com orçamento em centavos.
Trilha 3: quem mantém biblioteca ou monorepo
- Confira versões: o TypeScript 7.0 traz o
tscnativo, bem mais rápido. Se alguma ferramenta depende da API programática ou de plugins de linguagem (Vue, Svelte), mantenha o 6.0 instalado ao lado. - Limpe o tsconfig das opções descontinuadas e confirme que
typeslista os pacotes de@typesque você usa, já que no 6.0 eles não entram mais sozinhos. - Ligue
isolatedDeclarationse gere os.d.tscom o tsdown, conforme ferramentas. - Coloque
attw --pack .evitest --typecheckno CI e avalie publicar também no JSR, que aceita código TypeScript direto e gera os.d.tspara o npm.
Fontes para conferir e aprofundar
Documentação oficial, notas de lançamento e repositórios, consultados em 11 e 12 de novembro de 2026. As versões das bibliotecas são as marcadas como latest no registro do npm em 12/11/2026. A pesquisa passou por sete modelos do geo-orchestrator (Perplexity, GPT-6 Astra, Gemini 4 Argon, Claude Fable 5.1, Claude Opus 5.5 e GPT-6.1 Sol) e os exemplos de código foram compilados com o TypeScript 6.0.3.
Linguagem e compilador. Anúncio do TypeScript 7.0; TypeScript 7.1 beta; Anúncio do TypeScript 6.0; TypeScript 5.9; TypeScript 5.8; TypeScript 5.5; TypeScript 5.4; Repositório do typescript-go; Handbook do TypeScript; Referência do tsconfig; Proposta TC39 de anotações de tipo.
Execução. Node.js: módulos TypeScript; Deno: TypeScript; Bun: TypeScript; tsx.
Validação e esquemas. Zod 4; Valibot; ArkType; Effect Schema; Standard Schema; TypeBox.
APIs, rotas e dados. tRPC; Hono RPC; oRPC; TanStack Router; TanStack Start; Next.js e TypeScript; Drizzle ORM; Prisma ORM; Kysely; React com TypeScript.
Ferramentas. Biome; Oxlint com tipos; typescript-eslint; tsdown; Vitest: testes de tipos; Are The Types Wrong; JSR; ts-pattern; type-fest.
Guias desta coleção. React, Tailwind, tokens e CSS moderno, Navegação, Dashboards e Ícones e elementos visuais, cujos componentes, tokens e regras de acessibilidade aparecem tipados aqui.