Inferência de tipos é o TypeScript deduzindo um tipo que você não escreveu. Inicialize uma variável e ela assume o tipo do valor; retorne um valor de uma função e o tipo de retorno vem junto. A maior parte do código TypeScript tem bem menos anotações do que as pessoas imaginam.
Os comentários mostram o que o editor exibe quando você passa o mouse sobre cada nome. A última linha é um erro de compilação (TS2322), mesmo que count nunca tenha sido anotado; // @ts-expect-error marca o erro como esperado para que o resto do bloco rode.
let vs const: alargamento de literais
Um const nunca muda, então o TypeScript dá a ele o tipo mais estreito: o próprio valor literal. Um let recebe o tipo mais amplo, porque pode ser reatribuído depois.
let a = "left"; // string
const b = "left"; // "left"
let c = 42; // number
const d = 42; // 42
const e = true; // true
Isso importa quando uma função aceita só certos valores:
index.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.
Corrija com const ou anotando a variável: let dir: "left" | "right" = "left". Os tipos literais têm sua própria página, tipos literais.
Objetos e arrays alargam seus membros
As propriedades de um objeto literal são mutáveis, então elas se alargam como let, mesmo quando o próprio objeto é const. Os elementos de arrays fazem o mesmo. as const torna tudo readonly e mantém os tipos literais.
Inferência do tipo de retorno
O tipo de retorno de uma função é inferido a partir das instruções return. Quando há várias, o resultado é a union de todas elas.
Tipos de retorno inferidos servem para a maioria das funções. Escreva o tipo de retorno explicitamente em funções exportadas ou públicas quando quiser que a assinatura seja um contrato: uma mudança dentro do corpo que retorne outro tipo passa a falhar na própria função, e não em algum ponto de chamada distante.
Tipagem contextual: callbacks recebem seus tipos
Parâmetros não são inferidos a partir de como a função é chamada. Há duas exceções. Um parâmetro com valor padrão assume o tipo desse valor (function retry(times = 3) faz de times um number). E uma função escrita onde um tipo de função já é esperado recebe os tipos dos parâmetros desse contexto, então parâmetros de callbacks não precisam de anotação.
O mesmo vale para forEach, filter, reduce, callbacks de Promise, handlers de eventos e qualquer função que você passe para um parâmetro com um tipo de função declarado.
Parâmetros precisam de anotação
Sem um valor padrão ou um contexto como esse, o TypeScript não olha para como a função é chamada para adivinhar os tipos dos parâmetros. Com strict ativado, um parâmetro sem anotação é o erro TS7006:
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
Escreva function double(x: number) e o bloco imprime 42, com o tipo de retorno number inferido de x * 2.
Best common type
Para um array literal com valores de tipos diferentes, o TypeScript infere um array da union desses tipos. Ele não procura um tipo base comum que você não mencionou.
O array vazio é o caso que merece atenção. const list = [] começa como um array "em evolução", cujo tipo cresce a cada push; uma anotação, const list: string[] = [], deixa a intenção clara desde o início.
Quando anotar
| Situação | Anotar? |
|---|---|
| Variável inicializada com um valor | Não, o valor define o tipo |
Variável declarada sem valor (let result;) | Sim, let result: string; |
| Parâmetros de função | Sim (exceto callbacks com contexto ou com valor padrão) |
| Tipo de retorno de uma função interna pequena | Opcional |
| Tipo de retorno de uma função exportada ou pública | Recomendado, como contrato |
| Array ou objeto vazio que você preenche depois | Sim, const ids: number[] = [] |
| O valor deve manter seus tipos literais | Use as const |
| O valor precisa corresponder a um tipo mas manter o próprio tipo inferido | Use satisfies |
A forma mais rápida de ver o que foi inferido é passar o mouse sobre o nome no editor. Sem editor, atribua o valor a uma variável de tipo errado, como const probe: boolean = mixed;, e a mensagem de erro do compilador mostra o tipo inferido.
Perguntas frequentes
O que é inferência de tipos no TypeScript?
É o compilador deduzindo um tipo que você não escreveu. let count = 0 dá a count o tipo number, e uma função que retorna a + b com dois números recebe o tipo de retorno number. O tipo inferido é verificado exatamente como um tipo escrito.
Devo anotar todas as variáveis no TypeScript?
Não. Anote os parâmetros de funções (eles não são inferidos pelas chamadas, só por um valor padrão ou pelo contexto de um callback), os tipos de retorno de funções exportadas se você quer uma API estável e as variáveis declaradas sem valor. Deixe o TypeScript inferir o resto: const name: string = "Ada" repete o que o compilador já sabe.
Por que const infere um tipo literal e let infere string?
Um const nunca pode ser reatribuído, então const dir = "left" recebe o tipo literal "left". Um let poderia guardar depois qualquer outra string, então let dir = "left" é alargado para string. O mesmo alargamento acontece com as propriedades de um objeto literal, porque propriedades podem ser reatribuídas; as const impede isso.
Por que o TypeScript diz que um parâmetro tem implicitamente o tipo 'any'?
É o erro TS7006 de noImplicitAny, que faz parte de strict. O TypeScript não infere tipos de parâmetros a partir de como a função é chamada, então um parâmetro sem tipo seria any. Adicione uma anotação, function double(x: number), ou passe a função onde o tipo dela é conhecido pelo contexto, como um callback de map.
Como vejo o tipo que o TypeScript inferiu?
Passe o mouse sobre o nome no editor (o VS Code e a maioria dos editores mostram o tipo inferido em um tooltip). No código, atribuir o valor a uma variável de um tipo claramente errado faz o compilador mostrar o tipo inferido na mensagem de erro.