After gaining new insights, I have chosen to update this response.
In TypeScript, the never
type serves as the bottom type, encompassed within all other types, contrasting with the top type unknown
which encompasses all types.
Think of unknown
as a collection of all types and never
as a collection of no types. If we liken string | number
to a set containing string and number, then adding never
like string | number | never
still results in a set of string and number, similar to how adding 0 to one side of an equation doesn't alter the sum on the other side: 1 + 2 + 0 still equals 3. In this analogy, never
plays the role of 0.
The utility of never
becomes evident when handling function arguments. While a variable typed as unknown
can accept any value, specifying a type for a function argument such as f(callback: (a: T) => void)
necessitates that the callback's argument a
must accommodate T
. Thus, for example, if T
is boolean
, a callback accepting boolean | string
would suffice.
function f(callback: (a: boolean) => void) {}
f((a: boolean | string) => {}) // valid
Unlike a variable constrained by its type, in a callback type, T
acts as a lower constraint. To permit a function with an argument of any type, utilizing the bottom type never
replicates behavior akin to unknown
. This approach proves useful when imposing constraints without invoking the function, where (a: never) => void
signifies that the provided function can handle up to 1 argument of any type.
function brandFunction<F extends (arg: never) => void>(f: F): F & { brand: true } {
const newF = f as (F & { brand: true })
newF.brand = true
return newF
}
brandFunction(() => {}) // allowed
brandFunction((a: string) => {}) // allowed
brandFunction((a: string, b: string) => {}) // not allowed
The notion of never
being assignable to any type may seem peculiar, exemplified in this code snippet:
function throws(): never { throw new Error('this function does not return normally') }
const n: never = throws()
const a: string = n // permitted
This curious behavior originates from the fact that the throws()
function technically doesn't return never
, perhaps better termed as none
. Essentially, the function ceases execution upon encountering the error, and subsequent assignments remain unrealized. The debate arises on whether never
accurately conveys this concept or if a distinct unreachable
type, immune to assignment, might be more precise.
To delineate unreachable code post a call to throws()
, the ideal portrayal is through an unreachable
type. Though TypeScript acknowledges unreachable code following a return
or throw
statement, incorporating an unreachable
type to convey this to higher scopes remains elusive.
function f() {
throw new Error('this function does not return normally')
const a = 1 // TS recognises this as unreachable code
}
f()
const b = 2 // TS does NOT recognise this as unreachable code