Menu

Il tipo never in TypeScript: controlli esaustivi e never vs void

never è il tipo senza valori. È il tipo di ritorno delle funzioni che non finiscono mai, il tipo che resta quando il narrowing ha escluso ogni caso e lo strumento dietro i controlli esaustivi negli switch. Scopri da dove viene e in cosa si distingue da void.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

never è il tipo senza valori. Una funzione il cui tipo di ritorno è never non ritorna mai normalmente: lancia un errore o gira per sempre. Una variabile di tipo never indica codice che non può essere eseguito, ed è questo che rende possibili i controlli esaustivi.

return fail(...) compila in una funzione che restituisce number perché never è assegnabile a ogni tipo: un valore di tipo never non può mai arrivare davvero.

Funzioni che non ritornano mai

Due tipi di funzione non arrivano mai alla fine: quella che lancia sempre un errore e quella con un ciclo infinito. Il compilatore verifica l'affermazione. Una funzione annotata never la cui fine è raggiungibile dà l'errore 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...
  }
}

L'inferenza dipende dalla sintassi. Una dichiarazione di funzione che si limita a lanciare un errore viene inferita come se restituisse void, mentre una arrow function o un'espressione di funzione che si limita a lanciare viene inferita come never:

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

Il narrowing tratta una chiamata come un vicolo cieco solo quando il nome chiamato ha un tipo esplicito che restituisce never: una dichiarazione di funzione annotata : never, come fail qui sopra, oppure una variabile con un'annotazione di tipo, const fail: (m: string) => never = (m) => { throw new Error(m); }. Un never inferito non conta, e nemmeno const fail = (m: string): never => ..., dove è annotata solo la arrow function e non la variabile.

never vs void

voidnever
La funzione terminasìno (lancia un errore o gira per sempre)
Valore a runtimeundefinednessuno: la chiamata non ne produce mai uno
Codice dopo la chiamataraggiungibileirraggiungibile
Assegnabile ad altri tipisolo a void, unknown, anya ogni tipo
Uso tipicocallback, gestori di eventi, funzioni con effetti collateralifail(), assertNever(), cicli infiniti

La differenza pratica si vede nel narrowing. Dopo if (!user) fail("no user"), il compilatore sa che user è definito nella riga successiva solo se fail restituisce never. Con un ritorno void presume che l'esecuzione possa continuare.

Controlli esaustivi con never

Ogni case di uno switch su un'unione restringe il valore. Quando ogni membro è stato gestito, ciò che resta in default è never. Assegnarlo a una variabile never trasforma "ho gestito ogni caso" in qualcosa che il compilatore verifica:

Ora aggiungi un terzo membro all'unione senza aggiungere un caso:

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

L'errore nomina il membro che hai dimenticato. Aggiungi case "triangle": return (shape.base * shape.height) / 2; e compila di nuovo. L'helper assertNever è la forma riutilizzabile dello stesso controllo, e il suo throw conta ancora a runtime: i dati da JSON o da un client più vecchio possono contenere un kind che secondo i tipi è impossibile. Questo schema è la spina dorsale delle discriminated unions.

Narrowing fino a never

La stessa cosa succede con qualsiasi narrowing, non solo con switch. Una volta esclusa ogni possibilità, la variabile ha tipo never:

Se in seguito allarghi il parametro a string | number | boolean | bigint, la riga const nothing: never = x diventa un errore, che indica la funzione da aggiornare.

never sparisce nelle unioni

never è l'insieme vuoto di valori, quindi aggiungerlo a un'unione non cambia nulla: string | never è semplicemente string. È così che i conditional types filtrano le unioni. Un ramo che restituisce never rimuove quel membro:

I tipi integrati Exclude<T, U> ed Extract<T, U> funzionano esattamente così. In un'intersezione è il contrario: string & never è never.

I tipi impossibili diventano never

Un'intersezione che nessun valore può soddisfare si riduce a never:

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

Leggere una proprietà di un valore B riporta il 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 un tipo che hai costruito risulta never, cerca due parti che si contraddicono.

never, unknown e any

TipoValori che contieneAssegnabile aAccetta
unknownogni valoresolo unknown e anytutto
anyogni valoretutto tranne nevertutto
nevernessun valoretuttosolo never

unknown è la cima della gerarchia dei tipi e never è il fondo. any non fa affatto parte della gerarchia: disattiva i controlli.

Domande frequenti

Cos'è il tipo never in TypeScript?

never è il tipo che non ha valori. Non gli si può assegnare niente (tranne un altro never), ed è assegnabile a ogni tipo. Compare come tipo di ritorno delle funzioni che lanciano sempre un errore o girano all'infinito, come tipo di una variabile dopo che il narrowing ha escluso ogni possibilità, e come risultato di tipi impossibili come string & number.

Che differenza c'è tra never e void?

Una funzione che restituisce void termina normalmente; semplicemente non restituisce un valore utile (a runtime restituisce undefined). Una funzione che restituisce never non termina affatto: lancia un errore o gira per sempre. Il codice dopo una chiamata a una funzione never è irraggiungibile, e TypeScript lo tratta così durante il narrowing.

Come si fa un controllo esaustivo in TypeScript?

Nel ramo default di uno switch su un'unione, assegna il valore a una variabile di tipo never, oppure passalo a una funzione assertNever(value: never): never che lancia un errore. Se ogni caso è gestito, lì il valore è never e il codice compila. Se manca un caso, il compilatore segnala che il membro mancante non è assegnabile a never.

Perché il mio tipo è never?

Di solito perché TypeScript ha escluso ogni opzione con il narrowing (per esempio dopo aver controllato typeof x === "string" e typeof x === "number" su un string | number), oppure perché un'intersezione è impossibile: string & number, o due tipi oggetto la cui proprietà comune ha tipi letterali in conflitto. Passa il mouse sul tipo nell'editor per vedere quale passaggio l'ha prodotto.

Cosa significa "is not assignable to type never"?

Il codice ha provato a mettere un valore reale dove è ammesso solo never. In un controllo esaustivo significa che un membro dell'unione non è stato gestito. Altrove spesso significa che un array è stato inferito come never[] o che un'intersezione si è ridotta a never; aggiungi un'annotazione o correggi i tipi in conflitto.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA