Menu

Tipo never em TypeScript: verificação exaustiva e never vs void

never é o tipo sem nenhum valor. É o tipo de retorno de funções que nunca terminam, o tipo que sobra quando o narrowing descartou todos os casos e a ferramenta por trás das verificações exaustivas em switch. Veja de onde ele vem e como difere de void.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

never é o tipo sem nenhum valor. Uma função cujo tipo de retorno é never nunca retorna normalmente: ela lança um erro ou roda para sempre. Uma variável do tipo never marca código que não pode rodar, e é isso que torna possíveis as verificações exaustivas.

return fail(...) compila em uma função que retorna number porque never pode ser atribuído a qualquer tipo: um valor do tipo never nunca chega de fato.

Funções que nunca retornam

Dois tipos de função nunca chegam ao fim: uma que sempre lança erro e uma com loop infinito. O compilador confere a afirmação. Uma função anotada como never cujo final pode ser alcançado gera o erro TS2534, A function returning 'never' cannot have a reachable end point.

function fail(message: string): never {
  throw new Error(message);
}

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

A inferência muda conforme a sintaxe. Uma function declaration que só lança erro é inferida como retornando void, enquanto uma arrow function ou function expression que só lança erro é inferida como never:

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

O narrowing só trata uma chamada como beco sem saída quando o nome chamado tem um tipo explícito que retorna never: uma function declaration anotada com : never, como fail acima, ou uma variável com anotação de tipo, const fail: (m: string) => never = (m) => { throw new Error(m); }. Um never inferido não conta, nem const fail = (m: string): never => ..., em que só a arrow function é anotada e a variável não.

never vs void

voidnever
A função terminasimnão (lança erro ou roda para sempre)
Valor em tempo de execuçãoundefinednenhum: a chamada nunca produz um
Código depois da chamadaalcançávelinalcançável
Pode ser atribuído a outros tipossó a void, unknown, anya qualquer tipo
Uso típicocallbacks, event handlers, funções com efeitos colateraisfail(), assertNever(), loops infinitos

A diferença prática aparece no narrowing. Depois de if (!user) fail("no user"), o compilador sabe que user está definido na linha seguinte só se fail retornar never. Com retorno void, ele assume que a execução pode continuar.

Verificações exaustivas com never

Cada case de um switch sobre uma union estreita o valor. Quando todos os membros foram tratados, o que sobra no default é never. Atribuí-lo a uma variável never transforma "tratei todos os casos" em algo que o compilador verifica:

Agora adicione um terceiro membro à union sem adicionar um case:

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

O erro indica o membro que você esqueceu. Adicione case "triangle": return (shape.base * shape.height) / 2; e ele volta a compilar. O helper assertNever é a forma reutilizável da mesma verificação, e o throw dele continua importando em tempo de execução: dados vindos de JSON ou de um cliente antigo podem conter um kind que os tipos dizem ser impossível. Esse padrão é a base das discriminated unions.

Narrowing até never

O mesmo acontece com qualquer narrowing, não só com switch. Depois que todas as possibilidades são descartadas, a variável tem o tipo never:

Se mais tarde você ampliar o parâmetro para string | number | boolean | bigint, a linha const nothing: never = x vira um erro, apontando para a função que precisa ser atualizada.

never desaparece em unions

never é o conjunto vazio de valores, então adicioná-lo a uma union não muda nada: string | never é só string. É assim que os conditional types filtram unions. Um ramo que retorna never remove aquele membro:

Os tipos nativos Exclude<T, U> e Extract<T, U> funcionam exatamente assim. Em uma intersection é o contrário: string & never é never.

Tipos impossíveis viram never

Uma intersection que nenhum valor consegue satisfazer se reduz a never:

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

Ler uma propriedade de um valor B mostra o motivo: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. Quando um tipo que você montou acaba sendo never, procure duas partes que se contradizem.

never, unknown e any

TipoValores que contémPode ser atribuído aAceita
unknowntodos os valoressó unknown e anytudo
anytodos os valorestudo exceto nevertudo
nevernenhum valortudosó never

unknown é o topo da hierarquia de tipos e never é a base. O any nem faz parte da hierarquia: ele desliga as verificações.

Perguntas frequentes

O que é o tipo never no TypeScript?

never é o tipo que não tem nenhum valor. Nada pode ser atribuído a ele (exceto outro never), e ele pode ser atribuído a qualquer tipo. Ele aparece como tipo de retorno de funções que sempre lançam erro ou rodam para sempre, como tipo de uma variável depois que o narrowing descartou todas as possibilidades e como resultado de tipos impossíveis como string & number.

Qual é a diferença entre never e void?

Uma função que retorna void termina normalmente; ela só não retorna um valor útil (em tempo de execução retorna undefined). Uma função que retorna never não termina: lança um erro ou roda para sempre. O código depois de uma chamada a uma função never é inalcançável, e o TypeScript o trata assim no narrowing.

Como fazer uma verificação exaustiva no TypeScript?

No ramo default de um switch sobre uma union, atribua o valor a uma variável do tipo never, ou passe-o para uma função assertNever(value: never): never que lança um erro. Se todos os casos forem tratados, o valor é never ali e o código compila. Se faltar um caso, o compilador aponta que o membro que falta não pode ser atribuído a never.

Por que meu tipo virou never?

Normalmente porque o TypeScript descartou todas as opções no narrowing (por exemplo depois de verificar typeof x === "string" e typeof x === "number" em um string | number), ou porque uma intersection é impossível: string & number, ou dois tipos de objeto cuja propriedade em comum tem literal types conflitantes. Passe o mouse sobre o tipo no editor para ver qual passo o produziu.

O que significa "is not assignable to type never"?

O código tentou colocar um valor real onde só never é permitido. Em uma verificação exaustiva, significa que um membro da union não foi tratado. Em outros lugares, muitas vezes significa que um array foi inferido como never[] ou que uma intersection colapsou para never; adicione uma anotação ou corrija os tipos conflitantes.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR