Guia completo
TypeScript do zero: guia para tipar JavaScript
Entenda como TypeScript encontra erros antes da execução, configure um projeto e escolha entre tipos, interfaces, narrowing e generics sem decorar regras.
TypeScript é JavaScript com uma etapa de verificação de tipos antes da execução. Você continua escrevendo variáveis, funções, objetos e promessas, mas ganha um compilador capaz de avisar que um preço virou texto, que uma propriedade não existe ou que uma função recebeu o formato errado. Depois dessa conferência, os tipos são removidos e o navegador ou o Node executa JavaScript comum.
O resultado prático não é “um código sem bugs”. É uma mudança no momento em que uma parte importante dos bugs aparece. Em vez de o cliente descobrir no botão de finalizar compra, você vê o problema no editor ou no terminal, enquanto ainda está trabalhando naquela linha. Este guia mostra o modelo mental, a ordem de estudo e as decisões que realmente aparecem num projeto.
Se variáveis, funções e objetos ainda parecem assuntos separados, comece pelo guia de JavaScript. TypeScript não pula essa base: ele coloca contratos em cima dela. Se você já escreve pequenos programas em JavaScript, pode seguir daqui e abrir os exemplos num diretório de teste.
A conferência antes de o pedido sair
Imagine a cozinha de um restaurante. A pessoa monta o pedido, mas antes de o prato ir para a mesa existe uma conferência: número da mesa, itens, restrições e quantidade. Essa conferência não cozinha e não prova que a comida está gostosa. Ela apenas compara o pedido preparado com regras conhecidas e impede alguns enganos previsíveis de seguirem adiante.
O nome técnico dessa conferência é checagem estática de tipos. “Estática”
quer dizer que ela analisa o programa sem precisar executar cada caminho. O
compilador tsc lê o arquivo .ts, compara valores e operações com os tipos e,
se as regras estiverem satisfeitas, emite JavaScript. No mapa da analogia, o
pedido é o código-fonte, a ficha de conferência é o tipo, o conferente é o
compilador e o prato que de fato sai é o JavaScript.
O limite da comparação é importante: uma ficha correta não garante que o tomate está fresco. Do mesmo modo, TypeScript não sabe se uma API mentiu, se a regra de desconto está errada ou se o servidor caiu. Ele garante coerência entre o que você declarou e o que seu código consegue demonstrar antes de rodar.
Veja um bug que JavaScript aceita sem reclamar:
const pedido = {
subtotal: 120,
frete: '15',
};
const total = pedido.subtotal + pedido.frete;
console.log(total);O operador + também junta textos. Como frete é uma string, o resultado vira
'12015', não 135. O programa rodou e produziu uma resposta plausível o
suficiente para passar despercebida. Agora descreva o formato esperado:
type Pedido = {
subtotal: number;
frete: number;
};
const pedido: Pedido = {
subtotal: 120,
frete: 15,
};
const total = pedido.subtotal + pedido.frete;
console.log(total);Pedido é um type alias, um nome para um tipo. A tradução simples é “todo
valor tratado como Pedido precisa ter subtotal e frete numéricos”. A anotação
: Pedido liga esse contrato ao objeto. Ao trocar 15 por '15', a conferência
interrompe o fluxo antes de existir um total incorreto.
O contrato do TypeScript em uma frase
Um tipo descreve quais valores são permitidos e quais operações fazem sentido
para eles. Se uma variável é string, você pode pedir toUpperCase(). Se é
number, pode usar toFixed(). Se é string | number, primeiro precisa provar
qual dos dois valores recebeu. Essa descrição ajuda o editor a completar nomes,
o compilador a rejeitar combinações inválidas e outra pessoa a entender a
intenção da função.
Isso é diferente de converter um valor. Escrever const idade: number não
transforma '28' em 28. O sinal : introduz uma anotação de tipo e só
faz uma afirmação verificável. Para converter texto em número, você ainda usa
JavaScript, por exemplo Number(valor), e decide o que fazer se o resultado for
NaN.
Também é diferente de criar uma classe. Um tipo pode descrever um objeto que já
existe, sem gerar construtor, método ou qualquer linha extra no JavaScript. Essa
separação explica por que você não encontra type, interface ou string no
arquivo compilado: eles cumpriram seu trabalho durante o desenvolvimento.
O primeiro erro que você quer ver
Crie um arquivo pedido.ts com um frete textual e peça apenas a verificação:
type Pedido = {
subtotal: number;
frete: number;
};
const pedido: Pedido = {
subtotal: 120,
frete: '15',
};
console.log(pedido.subtotal + pedido.frete);Execute o compilador com npx tsc pedido.ts --noEmit. A opção --noEmit pede
para checar sem criar o .js. Com TypeScript 7.0.2, a parte central do retorno é:
Leia a mensagem de dentro para fora: foi encontrado string, o contrato exigia
number, e a atribuição não é permitida. O código TS2322 identifica essa família
de erro; arquivo, linha e coluna apontam onde começar. Não lute para “calar” a
mensagem com any. Corrija a origem ou converta e valide a entrada.
Esse erro é uma prova observável da promessa do TypeScript. Faça a primeira
microprática agora: troque '15' por 15, rode o mesmo comando e confirme que o
terminal termina sem diagnóstico. Depois acrescente console.log, compile sem
--noEmit e rode o JavaScript gerado. Você acabou de percorrer as duas etapas:
checagem e execução.
Inferir quando está óbvio, anotar quando existe fronteira
TypeScript possui inferência de tipos: ele deduz um tipo a partir do valor e
do contexto. Em const cidade = 'Recife', escrever : string repete uma
informação já visível. Em uma função pública, porém, os parâmetros formam uma
fronteira: vários lugares podem chamá-la, então declarar a entrada e a saída
expõe o contrato.
const taxa = 0.1;
function calcularDesconto(total: number, percentual: number): number {
return total * percentual;
}
const desconto = calcularDesconto(250, taxa);
console.log(desconto.toFixed(2));O compilador infere taxa e desconto como números. As anotações dos parâmetros
impedem alguém de chamar calcularDesconto('250', '10%'); o retorno explícito
protege a intenção caso uma manutenção devolva uma string formatada. Não existe
prêmio por colocar : number em todas as três constantes. Um bom tipo mostra a
fronteira e deixa o óbvio em silêncio.
Minha regra de projeto é anotar parâmetros de funções exportadas, retornos que carregam regra de negócio e estruturas compartilhadas. Em variáveis locais, prefiro inferência. Essa escolha reduz texto sem abrir mão das garantias onde uma mudança alcança mais gente.
O mapa do cluster em cinco etapas
A ordem abaixo acompanha uma aplicação pequena, não a ordem alfabética dos recursos da linguagem. Primeiro você instala a conferência; depois descreve valores; em seguida nomeia objetos; então trata alternativas; por último cria contratos reutilizáveis.
| Etapa | Pergunta que resolve | Lição prática |
|---|---|---|
| 1 | Como instalar, compilar e organizar a saída? | Instalar e configurar TypeScript |
| 2 | Como tipar texto, número, lista, objeto e alternativas? | Tipos básicos em TypeScript |
| 3 | Como escolher entre duas formas de nomear contratos? | Interface vs type |
| 4 | Como trabalhar com um valor que pode ter mais de um tipo? | Narrowing em TypeScript |
| 5 | Como reutilizar uma regra sem perder o tipo concreto? | Generics em TypeScript |
Não tente memorizar a lista inteira antes de escrever um programa. Termine cada
lição com o mesmo objeto Pedido e deixe a necessidade puxar o próximo recurso.
Quando uma entrada puder ser texto ou número, narrowing deixa de ser teoria.
Quando a mesma função precisar preservar Pedido e Cliente, generics ganha um
problema real para resolver.
Para ver os cinco assuntos atravessando as fronteiras de uma aplicação, siga o tutorial API Node com TypeScript, Express, Prisma e Docker. O projeto leva os tipos da entrada HTTP até o código que conversa com o banco.
Tipos básicos são o vocabulário do contrato
Os tipos cotidianos começam nos primitivos de JavaScript: string, number,
boolean, bigint, symbol, null e undefined. TypeScript acrescenta formas
de combiná-los: arrays, objetos, unions, literals e tuples. O nome “básico” não
significa descartável; quase todo contrato grande é uma composição dessas peças.
Uma union usa | para dizer “um ou outro”. Um literal type permite um
valor específico, não qualquer texto. Juntos, eles descrevem estados conhecidos:
type StatusPedido = 'recebido' | 'pago' | 'enviado';
function mensagem(status: StatusPedido): string {
if (status === 'recebido') return 'Aguardando pagamento';
if (status === 'pago') return 'Separando produtos';
return 'Pedido a caminho';
}
console.log(mensagem('pago'));Em JavaScript puro, um erro de digitação como 'paggo' atravessa a chamada e
pode cair num ramo errado. No contrato acima, só três textos entram. Na lição de
tipos básicos, você monta esse
vocabulário aos poucos e compara unknown com o atalho perigoso any.
Interface e type dão nome a formas diferentes
interface e type conseguem descrever muitos dos mesmos objetos. A discussão
fica confusa quando vira uma torcida por sintaxe. A pergunta útil é o que você
precisa modelar. Interface tem uma natureza aberta e pode ser estendida ou
mesclada. Type alias nomeia qualquer tipo, inclusive union, tuple e primitivo,
e combina estruturas por interseção.
interface Produto {
nome: string;
preco: number;
}
type ItemCarrinho = Produto & {
quantidade: number;
};
const item: ItemCarrinho = {
nome: 'Teclado',
preco: 200,
quantidade: 2,
};
console.log(`${item.quantidade}x ${item.nome}: R$ ${item.preco * item.quantidade}`);Aqui a interface comunica a forma estável de Produto; o type cria uma composição para o carrinho. Poderíamos resolver tudo com um só recurso, mas a mistura é intencional e legível. A lição interface vs type mostra declaração mesclada, extensão, union e o erro estrutural que acontece quando uma propriedade obrigatória falta.
Narrowing transforma possibilidade em certeza local
Quando um valor é string | number, TypeScript impede operações exclusivas de
string porque, naquele ponto, o número ainda é possível. Narrowing é o
estreitamento do tipo após uma verificação que prova qual caso está presente.
Em português direto: você testa a entrada e, dentro daquele caminho, o
compilador passa a saber mais sobre ela.
function normalizarCodigo(codigo: string | number): string {
if (typeof codigo === 'number') {
return codigo.toString().padStart(6, '0');
}
return codigo.trim().toUpperCase();
}
console.log(normalizarCodigo(42));
console.log(normalizarCodigo(' ab-9 '));typeof codigo === 'number' é um type guard, uma verificação reconhecida
pelo analisador. No primeiro ramo, codigo é número; depois do retorno, só pode
ser texto. Esse conhecimento é local e acompanha o fluxo de controle. A lição
de narrowing avança de typeof para
in, instanceof, predicados e unions discriminadas.
Generics preservam uma relação entre tipos
Um generic não quer dizer “aceita qualquer coisa e esquece o tipo”. Isso seria
any. Ele cria um parâmetro de tipo para preservar uma relação. Se a entrada é
Produto, a saída continua Produto; se é Cliente, continua Cliente. A função é
reutilizável sem apagar informação.
function primeiro<T>(itens: T[]): T | undefined {
return itens[0];
}
const primeiroNome = primeiro(['Ana', 'Bia']);
const primeiroNumero = primeiro([10, 20]);
console.log(primeiroNome?.toUpperCase());
console.log(primeiroNumero?.toFixed(2));T é um type parameter. Na primeira chamada ele vira string; na segunda,
number. O | undefined representa honestamente a lista vazia. Na lição de
generics, você também limita o parâmetro
com extends e liga chaves a objetos usando keyof.
Uma configuração pequena, mas deliberada
O arquivo tsconfig.json registra como o compilador deve analisar e emitir o
projeto. Comece com o modo estrito, separe fonte e saída e escolha um módulo que
combine com seu ambiente. Para um exercício executado como ES modules no Node
24, uma base possível é:
{
"compilerOptions": {
"target": "ES2024",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"rootDir": "src",
"outDir": "dist",
"noUncheckedIndexedAccess": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
}strict liga uma família de verificações, incluindo cuidado com null e
undefined. rootDir aponta onde mora o TypeScript; outDir, onde entra o
JavaScript gerado. noUncheckedIndexedAccess faz uma posição de array ou chave
dinâmica admitir undefined, porque o item pode não existir. sourceMap ajuda
o depurador a apontar o arquivo original.
Não copie configuração como receita universal. Front-end com Vite, biblioteca
publicada e API Node têm saídas diferentes. A lição de instalação explica cada
passo e mostra como separar npx tsc --noEmit para conferir de npx tsc para
emitir. A fonte da verdade de cada opção é a documentação oficial do TSConfig.
Tabela de decisão para um projeto
Depois de ensinar e revisar muitos códigos de iniciantes, minha decisão padrão é
esta: inferência no que é local e óbvio; contratos explícitos nas fronteiras;
unknown em dado externo; union discriminada para estados; generic apenas
quando há uma relação real a preservar. A tabela não é lei da linguagem. É uma
política de legibilidade, e você pode ajustá-la quando o projeto trouxer outra
pressão.
| Situação | Escolha inicial | Motivo e limite |
|---|---|---|
| Constante local com valor claro | Inferência | Evita repetir string ou number; anote se o contrato precisa ser mais amplo que o valor inicial. |
| Parâmetro de função exportada | Anotação explícita | A chamada atravessa arquivos; o contrato merece ficar visível. |
| Objeto público que pode ser estendido | interface |
Extensão e declaration merging fazem parte do recurso; não use abertura por acidente. |
| Union, tuple ou combinação de alternativas | type |
Type aliases nomeiam formas que uma interface não representa diretamente. |
| JSON, formulário ou resposta externa | unknown e validação |
Obriga a provar o formato; a anotação não valida o mundo fora do processo. |
| Estado com casos conhecidos | Union discriminada | Cada caso carrega seus próprios dados e o switch pode ser exaustivo. |
| Função que mantém o tipo da entrada na saída | Generic | Preserva a relação; se o tipo não influencia outro ponto, talvez não precise de T. |
| Pressa para silenciar um erro | Investigar antes de usar any |
any desliga a conferência e espalha incerteza para as próximas operações. |
Um sinal de tipo bem escolhido é conseguir explicá-lo como regra do domínio: “pedido pago tem data de pagamento”; “esta busca devolve o mesmo tipo de item que recebeu”; “esta entrada ainda não foi validada”. Quando a explicação vira “usei porque o compilador mandou”, pare e volte à informação que o valor representa.
O que os tipos não enxergam
TypeScript verifica o código disponível na compilação. Uma resposta HTTP chega
depois, durante a execução. Você pode afirmar que ela é Cliente, mas uma
afirmação não muda os bytes recebidos. Se a API devolver { nome: null }, o
objeto continuará assim mesmo que uma interface diga nome: string.
type Cliente = {
nome: string;
};
const texto = '{"nome":null}';
const cliente = JSON.parse(texto) as Cliente;
console.log(cliente.nome);as Cliente é uma type assertion: você diz ao compilador “confie em mim”.
Não há validação gerada. Uma fronteira segura começa com unknown, verifica se
há objeto, propriedade e tipo correto, e só então devolve Cliente. Bibliotecas de
schema automatizam esse trabalho, mas a responsabilidade continua sendo de
tempo de execução.
Tipos também não provam regra de negócio. Dois números bem tipados ainda podem ser somados na ordem errada; um desconto pode aceitar 150%; uma data válida pode estar no passado quando deveria estar no futuro. Testes automatizados, validações e revisão continuam complementando a checagem estática.
Três atalhos que cobram a conta depois
O primeiro atalho é espalhar any. Ele não quer dizer “qualquer tipo com
segurança”; quer dizer “não confira as próximas operações”. Um valor any passa
por parâmetros, propriedades e retornos carregando essa dispensa. Em migração de
um projeto grande, pode ser uma ponte temporária, desde que exista um limite
visível. Para dado novo e desconhecido, unknown força a prova antes do uso e
preserva o propósito da checagem.
O segundo é usar as como conversão. Type assertion é o nome correto, e a
palavra assertion importa: você afirma que sabe algo que o compilador não
conseguiu demonstrar. Nenhum valor é transformado. Se uma entrada realmente
precisa virar número, execute Number, confira Number.isNaN e decida como
responder ao valor inválido. Reserve as para situações em que outra evidência
garante o formato, mas a análise não consegue representá-la.
O terceiro é instalar tipos e desligar strict sempre que surge atrito. No
começo isso parece acelerar a entrega, mas permite que null, parâmetros
implícitos e retornos imprecisos atravessem as fronteiras. Quando o projeto
cresce, religar a opção transforma um problema localizado em centenas de
diagnósticos. Num projeto novo, prefiro começar estrito e resolver a incerteza no
momento em que ela nasce.
Há ainda uma distinção operacional: compilador, editor e ferramenta de build não
são a mesma coisa. O editor oferece feedback enquanto você digita. O tsc é a
referência compartilhável da checagem. Vite, esbuild ou outra ferramenta pode
remover a sintaxe TypeScript e criar o bundle sem verificar todos os tipos. Por
isso, um build visualmente bem-sucedido não substitui necessariamente
tsc --noEmit. Descubra qual ferramenta assume cada etapa no seu projeto e
registre a checagem num script que também roda no CI.
Uma microprática útil é procurar any, as e ! no primeiro projeto. Não
apague de modo mecânico. Para cada ocorrência, escreva qual evidência justificou
o atalho. Se não houver resposta observável, troque por um contrato preciso, um
guard ou uma validação de runtime. O objetivo não é zerar operadores; é evitar
que uma afirmação sem prova se disfarce de segurança.
A ordem que evita estudar só sintaxe
Instale TypeScript num diretório pequeno e faça um pedido aparecer no terminal. Depois provoque um erro de tipo e leia a mensagem completa. Só então avance para listas, objetos e unions. Essa sequência mantém cada nome técnico ligado a um efeito observável.
Na etapa de interface e type, modele dois objetos do seu projeto em vez de
copiar Pessoa e Animal. Em narrowing, faça uma entrada admitir duas formas e
trate as duas. Em generics, espere surgir repetição antes de generalizar. O
recurso deve responder a uma dor que você já viu no código.
Use a trilha de TypeScript como sequência navegável. Ao concluir cada lição, guarde o código num repositório e faça um commit com o erro antes da correção e outro depois. Esse histórico torna sua aprendizagem revisável: você não precisa confiar na sensação de que entendeu.
Missão: blindar um carrinho pequeno
Crie src/carrinho.ts com um tipo Item que tenha nome, preco e
quantidade. Escreva calcularTotal(itens) e formatarResumo(item). Permita
que o status do carrinho seja apenas 'aberto', 'pago' ou 'cancelado'.
Compile e imprima o total de dois itens.
Depois faça três mudanças controladas: passe preço como texto, retire uma
propriedade obrigatória e use o status 'finalizado'. O critério de sucesso é o
compilador rejeitar as três alterações antes da execução. Corrija uma de cada
vez, rode o JavaScript e confirme o total esperado. Por fim, anote numa frase
qual erro TypeScript encontrou e qual regra de negócio ele não seria capaz de
encontrar.
Quando essa missão funcionar, você terá o núcleo do método: contrato explícito, erro reproduzido, correção comprovada e limite entendido. A linguagem deixa de ser uma coleção de sinais depois que cada tipo passa a representar uma decisão real do programa.
Trilha
TypeScript
Tipos no JavaScript que você já escreve: o erro aparece no editor, antes do usuário encontrar.
Perguntas frequentes
Preciso dominar JavaScript antes de estudar TypeScript?
TypeScript deixa a aplicação mais rápida?
TypeScript valida os dados que chegam de uma API?
Devo escrever o tipo de toda variável?
Interface ou type: qual devo usar por padrão?
O código deste guia foi executado em Node 24.16.0 com TypeScript 7.0.2, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- TypeScript Handbook — The Basics — typescriptlang.org
- TypeScript Handbook — Everyday Types — typescriptlang.org
- TypeScript Handbook — Narrowing — typescriptlang.org
- TypeScript Handbook — Generics — typescriptlang.org