Variables
As regras de const, let e valor que não muda depois de atribuído vêm do JavaScript e continuam
as mesmas. O que o TypeScript acrescenta é a decisão de tipo: quando anotar, quando deixar a
type inference (inferência de tipo, o compilador deduzindo o tipo a partir do valor) trabalhar
sozinha, e como usar as const, satisfies e unknown para o contrato não abrir buraco.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| type inference (inferência de tipo) | Compilador deduz o tipo a partir da atribuição; padrão preferido |
| type annotation (anotação de tipo) | Declaração explícita do tipo (const x: number = 0); usar quando agrega informação |
as const (afirmação literal) | Congela o valor como literal exato e torna campos readonly |
satisfies (operador de conformidade) | Valida que o valor cumpre o tipo sem alargar a tipagem inferida |
any (tipo escape) | Desliga a checagem naquela variável; anti-padrão, salvo em um limite controlado do sistema |
unknown (tipo seguro de origem desconhecida) | Tipo amplo que obriga a checar antes de usar; o substituto correto de any |
| narrowing (estreitamento de tipo) | Checagem que reduz o tipo até o compilador saber com o que está lidando |
non-null assertion (afirmação de não-nulo, !) | Força não-nulo sem checagem; evitar fora de testes ou inicialização garantida |
| definite assignment (atribuição garantida) | let x!: T: promete ao compilador que será atribuído antes do uso |
O que é inferência de tipo
Inferência é o compilador descobrir o tipo sozinho, olhando para o valor que você atribuiu. Quando
você escreve const userName = "Alice", ninguém disse ao TypeScript que userName é uma string.
Ele leu o valor "Alice", concluiu que só pode ser uma string, e passou a tratar a variável assim
daí em diante. A partir desse momento userName.toUpperCase() é aceito, e userName * 2 é acusado
como erro, exatamente como se o tipo estivesse escrito na declaração.
Isso vale para o resto da linguagem, e não só para variáveis. O retorno de uma função é inferido a
partir do que ela devolve; o tipo de um item dentro de um map é inferido a partir do array; o tipo
de um objeto é inferido campo a campo. Escrever tipo à mão é a exceção, reservada para os casos em
que não existe valor de onde inferir, ou em que o tipo inferido é mais amplo do que se quer.
Vale saber o limite da inferência: ela olha o valor daquele momento, e nada mais. Um array vazio
não tem elemento de onde tirar o tipo, uma variável declarada sem valor não tem valor nenhum, e uma
resposta que chegou pela rede é um unknown que o compilador não tem como adivinhar. Nesses três
casos a anotação deixa de ser redundância e vira a única fonte de informação.
Deixe o compilador inferir o tipo óbvio
O TypeScript deriva o tipo a partir do valor atribuído. Escrever const userName: string = "Alice"
repete no tipo o que o valor já mostra, e o leitor passa por duas informações para receber uma. A
anotação vale quando ela acrescenta algo que o valor não diz.
❌ Ruim: anotação repete o que a atribuição já diz
const userName: string = "Alice";
const isActive: boolean = true;
const MAX_RETRIES: number = 3;
const orders: Order[] = [];
✅ Bom: inferência quando o tipo é óbvio
const userName = "Alice";
const isActive = true;
const MAX_RETRIES = 3;
const orders: Order[] = []; // anotação necessária: array vazio não tem tipo inferível
Anote quando não há valor de onde inferir
A inferência precisa de um valor inicial para funcionar. Sem ele, o compilador não tem de onde tirar
o tipo: uma variável declarada vazia vira any, e um array vazio vira never[], que não aceita
nenhum elemento depois. Nos dois casos a anotação é o que informa o tipo.
❌ Ruim: tipo implícito `any` sem aviso visual
let currentUser; // any: sem tipo, sem proteção
const results = []; // never[]: TypeScript não sabe o tipo dos elementos
✅ Bom: anotação explícita onde a inferência não alcança
let currentUser: User | null = null;
const results: Order[] = [];
Use unknown no lugar de any
any desliga a checagem de tipos naquela variável, e o efeito atravessa tudo que sai dela:
data.user.name compila mesmo quando a resposta não tem user, e o erro só aparece em runtime,
no navegador do usuário. unknown diz a mesma coisa (o valor chegou de fora e não se sabe o que é)
sem abrir mão da checagem: para usar o valor, é preciso primeiro provar ao compilador o que ele é.
❌ Ruim: any apaga todo o benefício do TypeScript
async function fetchExternalData(): Promise<any> {
const response = await fetch(apiUrl);
return response.json(); // qualquer coisa pode sair daqui sem aviso
}
const data = await fetchExternalData();
data.user.name; // TypeScript aceita, mas pode explodir em runtime
✅ Bom: unknown obriga a checar o formato antes de usar o valor
async function fetchExternalData(): Promise<unknown> {
const response = await fetch(apiUrl);
return response.json();
}
const raw = await fetchExternalData();
if (!isApiResponse(raw)) throw new ValidationError({ message: "Unexpected response shape." });
const data = raw; // narrowado para ApiResponse: seguro usar
as const prende o valor ao literal exato
Sem as const, o compilador vê pending: "pending" e infere string, porque supõe que o campo
pode receber outra string depois. O tipo aceita qualquer string, e a restrição que o objeto parecia
declarar não existe. Com as const, cada valor vira o literal exato ("pending", e não string),
e o objeto inteiro passa a ser somente leitura. É o que permite derivar dele um union type com os
valores válidos, o padrão que substitui o enum.
❌ Ruim: tipo inferido como string, perde a especificidade
const ORDER_STATUS = {
pending: "pending",
approved: "approved",
cancelled: "cancelled",
};
// tipo inferido: { pending: string; approved: string; cancelled: string }
// ORDER_STATUS.pending é string: qualquer string passa
function updateStatus(status: string) { /* ... */ } // sem restrição real
✅ Bom: as const preserva os literais
const ORDER_STATUS = {
pending: "pending",
approved: "approved",
cancelled: "cancelled",
} as const;
type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];
// OrderStatus = "pending" | "approved" | "cancelled"
function updateStatus(status: OrderStatus) { /* ... */ } // só aceita os valores válidos
satisfies confere o tipo e preserva o literal
Anotar const createOrder: RouteConfig faz o compilador tratar o objeto como um RouteConfig
qualquer, e createOrder.method volta a ser o union inteiro ("GET" | "POST" | "PUT" | "DELETE").
A informação de que ali dentro o método é "POST" se perde na anotação.
satisfies faz a mesma conferência (o objeto cumpre o contrato? falta campo? sobra campo?) e
mantém o tipo que foi inferido do valor. createOrder.method continua sendo "POST".
❌ Ruim: a anotação troca o literal pelo tipo base
interface RouteConfig {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
}
const createOrder: RouteConfig = {
path: "/orders",
method: "POST",
};
createOrder.method; // tipo: "GET" | "POST" | "PUT" | "DELETE", perde a especificidade
✅ Bom: satisfies valida e preserva o tipo literal
const createOrder = {
path: "/orders",
method: "POST",
} satisfies RouteConfig;
createOrder.method; // tipo: "POST", literal preservado
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.