As regras abaixo são as que mais evitam bugs em código TypeScript real. Cada uma mostra primeiro a versão comum e depois a melhor, em código que você pode rodar. A primeira regra é a mais importante: pare de usar any.
Use unknown em vez de any
any desliga a verificação de tipos para um valor e para tudo o que é calculado a partir dele. unknown também aceita qualquer valor, mas você precisa verificá-lo antes de usar, o que coloca a verificação no ponto onde os dados entram no seu programa:
As fronteiras são os lugares onde os tipos deixam de ser garantidos: JSON.parse, respostas de fetch, localStorage, entradas de formulário, variáveis de ambiente e mensagens de outros processos. Valide ali com um type guard ou uma biblioteca de schema, e o resto do código pode confiar nos seus tipos.
Mantenha o strict ativado
strict é o padrão no TypeScript 7; não o desative. Adicione as verificações que ele deixa de fora e que mais pegam bugs:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}
noUncheckedIndexedAccess faz arr[i] e record[key] incluírem undefined, que é o que eles retornam para um índice inexistente. noImplicitOverride obriga um método de subclasse a dizer override, e noFallthroughCasesInSwitch rejeita um case que continua no seguinte.
Deixe a inferência trabalhar
Anote o que o TypeScript não tem como saber: parâmetros de funções e os tipos de retorno de funções usadas por outros módulos. Deixe variáveis locais e parâmetros de callbacks para a inferência. Uma anotação desnecessária não é só ruído; ela pode deixar um tipo mais amplo que o valor:
Sem o comentário @ts-expect-error, setStatus(annotated) é o erro TS2345. O const inferido mantém o tipo literal "active", então é aceito. Passe o mouse sobre uma variável no editor para ver o que foi inferido antes de adicionar um tipo.
Prefira union types a enums
Uma union de literais de string dá autocomplete e verificações exaustivas sem código gerado. Quando você também precisa da lista de valores em tempo de execução, derive o tipo de um array as const:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
function canEdit(role: Role): boolean {
return role !== "viewer";
}
console.log(canEdit("editor")); // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]
canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
Um enum Role { Admin, Viewer } vira na compilação um objeto com reverse mapping, e um parâmetro de enum numérico aceita qualquer variável number, mesmo uma com um valor que o enum não lista. Enums também não rodam com o type stripping do Node (TypeScript enum is not supported in strip-only mode). As vantagens e desvantagens estão comparadas na página de enums.
Verifique objetos de configuração com satisfies
Anotar um objeto com um tipo amplo como Record<string, Route> verifica os valores, mas esquece as chaves. satisfies verifica a mesma coisa e mantém o tipo exato:
Use uma anotação quando a variável deve ter exatamente o tipo declarado (um parâmetro de função, um valor que você vai reatribuir). Use satisfies para tabelas de consulta, mapas de rotas, tokens de tema e outros objetos constantes.
Modele estado com discriminated unions
Um único objeto com campos opcionais permite estados que não podem acontecer: loading: true junto com um error, ou data ausente depois do sucesso. Uma union de objetos com uma tag compartilhada só permite os estados reais, e cada ramo enxerga só os seus próprios campos:
A linha com never é a verificação exaustiva. Quando alguém adiciona um estado e esquece de tratá-lo, o build quebra nessa linha:
O erro é index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. Ele diz qual case não foi tratado. Adicione case "cancelled": e compila.
Evite ! e as
A non-null assertion x! e a type assertion x as T mandam o compilador parar de verificar. Nenhuma das duas muda o valor em tempo de execução, então uma assertion errada vira uma quebra mais tarde, longe da causa:
Troque ! por ?., ??, um return antecipado ou um erro lançado com uma mensagem útil. Troque as por um type guard que de fato testa o valor. A única assertion sempre segura é as const, porque ela só deixa um tipo mais estreito e somente leitura. Uma boa configuração de lint (as regras no-non-null-assertion e no-explicit-any do typescript-eslint) aponta o resto.
Torne os dados readonly
Marque propriedades e arrays como readonly quando o código não deve alterá-los. Aí o compilador rejeita push, sort e atribuições, e as funções retornam novos valores em vez de modificar suas entradas:
readonly só é verificado em tempo de compilação e só um nível abaixo: ele não congela o objeto em tempo de execução. Mesmo assim, isso basta para pegar a modificação acidental de estado compartilhado, que é a causa da maioria desses bugs.
Perguntas frequentes
Devo usar any no TypeScript?
Quase nunca em código de aplicação. any desliga a verificação para o valor e para tudo o que deriva dele. Use unknown para valores cujo tipo você ainda não conhece e estreite-os com verificações; reserve any para raras válvulas de escape, com um comentário explicando o motivo.
Devo anotar todas as variáveis no TypeScript?
Não. Deixe o TypeScript inferir variáveis locais e parâmetros de callbacks. Anote os parâmetros de funções (eles não podem ser inferidos) e os tipos de retorno de funções exportadas, para que uma mudança dentro da função não altere sem aviso o seu tipo público.
Enums são má prática no TypeScript?
Não são errados, mas muitos times os evitam. Um enum gera código em tempo de execução, não roda com o type stripping do Node, e um parâmetro de enum numérico aceita qualquer variável number, seja qual for o valor dela. Uma union de literais de string, ou um array as const com um tipo derivado, dá o mesmo autocomplete e as mesmas verificações sem código gerado.
Quando devo usar type assertions com as?
Só quando você sabe algo que o compilador não tem como saber, e de preferência logo depois de uma verificação que prove isso. as não muda nenhum valor em tempo de execução, então {} as User compila e depois não tem name. Um type guard que testa o valor é a ferramenta mais segura na maioria dos casos.
Quais configurações de tsconfig são melhores para um novo projeto TypeScript?
Mantenha strict ativado (o padrão no TypeScript 7) e adicione noUncheckedIndexedAccess. Muitos projetos também ativam noImplicitOverride, noFallthroughCasesInSwitch e verbatimModuleSyntax. A configuração que tsc --init gera define strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes e verbatimModuleSyntax, entre outras.