You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TypeScript’s never type is very under-discussed, because it’s not nearly as ubiquitous or inescapable as other types. A TypeScript beginner can probably ignore never type as it only appears when dealing with advanced types, such as conditional types, or reading their cryptic type error messages.
The never type does have quite a few good use cases in TypeScript. However, it also has its own pitfalls you need to be careful of.
To fully understand never type and its purposes, we must first understand what a type is, and what role it plays in a type system.
A type is a set of possible values. For example, string type represents an infinite set of possible strings. So when we annotate a variable with type string, such a variable can only have values from within that set, i.e. strings:
In TypeScript, never is an empty set of values. In fact, in Flow, another popular JavaScript type system, the equivalent type is called exactly empty
Since there’s no values in the set, never type can never (pun-intended) have any value, including values of any type. That’s why never is also sometimes referred to as an uninhabitable type or a bottom type.
While you probably wouldn’t find yourself use never a lot, there are quite a few legit use cases for it:
Annotate inadmissible function parameters to impose restrictions#
Since we can never assign a value to never type, we can use it to impose restrictions on functions for various use cases.
Ensure exhaustive matching within switch and if-else statement#
If a function can only take one argument of never type, that function can never be called with any non-never value (without the TypeScript compiler yelling at us):
We can use such a function to ensure exhaustive matching within switch and if-else statement: by using it as the default case, we ensure that all cases are covered, since what remains must be of type never. If we accidentally leave out a possible match, we get a type error. For example:
Let’s say we have a function that accepts a parameter of either the type VariantA or VariantB. But, the user mustn’t pass a type encompassing all properties from both types, i.e., a subtype of both types.
We can leverage a union type VariantA | VariantB for the parameter. However, since type compatibility in TypeScript is based on structural subtyping, passing an object type that has more properties than the parameter’s type has to a function is allowed (unless you pass object literals):
The above code snippet doesn't give us a type error in TypeScript.
By using never, we can partially disable structural typing and prevent users from passing object values that include both properties:
Let’s say we want to create a Cache instance to read and store data from/to it:
Now, for some reason we want to have a read-only cache only allowing for reading data via the get method. We can type the argument of the put method as never so it can’t accept any value passed in it:
Unrelated to never type, as a side note, this might not be a good use case of derived classes. I am not really an expert on object-oriented programming, so please use your own judgment.
When using infer to create an additional type variable inside a conditional type, we must add an else branch for every infer keyword:
In my previous post I mentioned how you can create declare “local (type) variable” together with extends infer. Check it out here if you haven’t seen it.
Beside denoting impossible branches, never can be used to filter out unwanted types in conditional types.
As we have discussed this before, when used as a union member, never type is removed automatically. In other words, the never type is useless in a union type.
When we are writing a utility type to select union members from a union type based on certain criteria, never type's uselessness in union types makes it the perfect type to be placed in else branches.
Let's say we want a utility type ExtractTypeByName to extract the union members with the name property being string literal foo and filter out those that don't match:
Here are a list of steps TypeScript folllows to evaluate and get the resultant type:
Conditional types are distributed over union types (namely, Name in this case):
Substitue the implementation and evaluate separately
In TypeScript, types are immutable. If we want to delete a property from an object type, we must create a new one by transforming and filtering the existing one. When we conditionally re-map keys in mapped types to never, those keys get filtered out.
Here’s an example for a Filter type that filters out object type properties based on their value types.
When we type a function’s return value as never, that means the function never returns control to the caller when it finishes executing. We can leverage that to help control flow analysis to narrow down types.
A function can never return for several reasons: it might throw an exception on all code paths, it might loop forever, or it exits from the program e.g. process.exit in Node.
In the following code snippet, we use a function that returns never type to strip away undefined from the union type for foo:
Or invoke throwError after || or ?? operator:
Denote impossible intersections of incompatible types#
This one might feel more like a behavior/characteristic of the TypeScript language than a practical application for never. Nevertheless, it’s vital for understanding some of the cryptic error messages you might come across.
You can get never type by intersecting incompatible types
And you get never type by intersecting any types with never
When intersecting object types, depending on whether or not the disjoint properties are considered as discriminant properties (basically literal types or unions of literal types), you might or might not get the whole type reduced to never
In this example only name property becames never since string and number are not discriminant properties
In the following example, the whole type Baz is reduced to never because a boolean is a discriminant property (a union of true | false)
You might have gotten error messages involving an unexpected never type from code you didn’t annotate with never explicitly. That’s usually because the TypeScript compiler intersects the types. It does this implicitly for you to retain type safety and to ensure soundness.
Here’s an example (play with it in TypeScript playground) that I used in my previous blog post on typing polymorphic functions:
The function returns either a number, a string, or a boolean depending on the type of argument we pass. We use an indexes access ReturnTypeByInputType[T] to retrieve the corresponding return type.
However, for every return statement we have a type error, namely: Type X is not assignable to type 'never' where X is string or number or boolean, depending on the branch.
This is where TypeScript tries to help us narrow down the possibility of problematic states in our program: each return value should be assignable to the type ReturnTypeByInputType[T] (as we annotated in the example) where ReturnTypeByInputType[T] at runtime could end up being either a number, a string, or a boolean.
Type safety can only be achieved if we make sure that the return type is assignable to all possible ReturnTypeByInputType[T], i.e. the intersection of number , string, and boolean. And what’s the intersection of these 3 types? It’s exactly never as they are incompatible with each other. That’s why we are seeing never in the error messages.
To work around this, you must use type assertions (or function overloads):
return Math.floor(Math.random() * 10) as ReturnTypeByInputType[T]
return Math.floor(Math.random() * 10) as never
Maybe another more obvious example:
obj[key] could end up being either a string or a number depending on the value of key at runtime. Therefore, TypeScript added this constraint, i.e., any values we write to obj[key] must be compatible with both types, string and number, just to be safe. So, it intersects both types and gives us never type.
A complete guide to TypeScript’s never type - zhenghao
https://ift.tt/8Nbuk9S
A complete guide to TypeScript’s never type
#typescriptPublished on 04 March, 2022Last updated on 11 March, 2022
TypeScript’s
never
type is very under-discussed, because it’s not nearly as ubiquitous or inescapable as other types. A TypeScript beginner can probably ignorenever
type as it only appears when dealing with advanced types, such as conditional types, or reading their cryptic type error messages.The
never
type does have quite a few good use cases in TypeScript. However, it also has its own pitfalls you need to be careful of.In this blog post, I will cover:
never
type and why we need it.never
.What is never type#
To fully understand
never
type and its purposes, we must first understand what a type is, and what role it plays in a type system.A type is a set of possible values. For example,
string
type represents an infinite set of possible strings. So when we annotate a variable with typestring
, such a variable can only have values from within that set, i.e. strings:In TypeScript,
never
is an empty set of values. In fact, in Flow, another popular JavaScript type system, the equivalent type is called exactly emptySince there’s no values in the set,
never
type can never (pun-intended) have any value, including values ofany
type. That’s whynever
is also sometimes referred to as an uninhabitable type or a bottom type.The bottom type is how the TypeScript Handbook defines it. I found it makes more sense when we place
never
in the type hierarchy tree, a mental model I use to understand subtypingThe next logical question is, why do we need
never
type?Why we need never type#
Just like we have zero in our number system to denote the quantity of nothing, we need a type to denote impossibility in our type system.
The word "impossibility" itself is vague. In TypeScript, “impossibility” manifests itself in various ways, namely:
process.exit
in Nodevoid
, asvoid
means a function doesn’t return anything useful to the caller.promise
How never works with unions and intersections#
Analogous to how number zero works in addition and multiplication,
never
type has special properties when used in union types and intersection types:never
gets dropped from union types, similiar to when zero added to a number gives the same number.type Res = never | string // string
never
overrides other types in intersection types, similiar to when zero multiplying a number gives zero.type Res = never & string // never
These two behaviors/characteristics of
never
type lay the foundation for some of its most important use cases that we will see later on.How to use never type#
While you probably wouldn’t find yourself use
never
a lot, there are quite a few legit use cases for it:Annotate inadmissible function parameters to impose restrictions#
Since we can never assign a value to
never
type, we can use it to impose restrictions on functions for various use cases.Ensure exhaustive matching within switch and if-else statement#
If a function can only take one argument of
never
type, that function can never be called with any non-never
value (without the TypeScript compiler yelling at us):We can use such a function to ensure exhaustive matching within switch and if-else statement: by using it as the default case, we ensure that all cases are covered, since what remains must be of type
never
. If we accidentally leave out a possible match, we get a type error. For example:Partially disallow structural typing#
Let’s say we have a function that accepts a parameter of either the type
VariantA
orVariantB
. But, the user mustn’t pass a type encompassing all properties from both types, i.e., a subtype of both types.We can leverage a union type
VariantA | VariantB
for the parameter. However, since type compatibility in TypeScript is based on structural subtyping, passing an object type that has more properties than the parameter’s type has to a function is allowed (unless you pass object literals):The above code snippet doesn't give us a type error in TypeScript.
By using
never
, we can partially disable structural typing and prevent users from passing object values that include both properties:Prevent unintended API usage#
Let’s say we want to create a
Cache
instance to read and store data from/to it:Now, for some reason we want to have a read-only cache only allowing for reading data via the
get
method. We can type the argument of theput
method asnever
so it can’t accept any value passed in it:Denote theoretically unreachable conditional branches#
When using
infer
to create an additional type variable inside a conditional type, we must add an else branch for everyinfer
keyword:In my previous post I mentioned how you can create declare “local (type) variable” together with
extends infer
. Check it out here if you haven’t seen it.Filter out union members from union types#
Beside denoting impossible branches,
never
can be used to filter out unwanted types in conditional types.As we have discussed this before, when used as a union member,
never
type is removed automatically. In other words, thenever
type is useless in a union type.When we are writing a utility type to select union members from a union type based on certain criteria,
never
type's uselessness in union types makes it the perfect type to be placed in else branches.Let's say we want a utility type
ExtractTypeByName
to extract the union members with thename
property being string literalfoo
and filter out those that don't match:Here are a list of steps TypeScript folllows to evaluate and get the resultant type:
Name
in this case):never
from the unionFilter out keys in mapped types#
In TypeScript, types are immutable. If we want to delete a property from an object type, we must create a new one by transforming and filtering the existing one. When we conditionally re-map keys in mapped types to
never
, those keys get filtered out.Here’s an example for a
Filter
type that filters out object type properties based on their value types.Narrow types in control flow analysis#
When we type a function’s return value as
never
, that means the function never returns control to the caller when it finishes executing. We can leverage that to help control flow analysis to narrow down types.In the following code snippet, we use a function that returns
never
type to strip awayundefined
from the union type forfoo
:Or invoke
throwError
after||
or??
operator:Denote impossible intersections of incompatible types#
This one might feel more like a behavior/characteristic of the TypeScript language than a practical application for
never
. Nevertheless, it’s vital for understanding some of the cryptic error messages you might come across.You can get
never
type by intersecting incompatible typesAnd you get
never
type by intersecting any types withnever
When intersecting object types, depending on whether or not the disjoint properties are considered as discriminant properties (basically literal types or unions of literal types), you might or might not get the whole type reduced to
never
In this example only
name
property becamesnever
sincestring
andnumber
are not discriminant propertiesIn the following example, the whole type
Baz
is reduced tonever
because a boolean is a discriminant property (a union oftrue | false
)Check out this PR to learn more.
How to read never type (from error messages)#
You might have gotten error messages involving an unexpected
never
type from code you didn’t annotate withnever
explicitly. That’s usually because the TypeScript compiler intersects the types. It does this implicitly for you to retain type safety and to ensure soundness.Here’s an example (play with it in TypeScript playground) that I used in my previous blog post on typing polymorphic functions:
The function returns either a number, a string, or a boolean depending on the type of argument we pass. We use an indexes access
ReturnTypeByInputType[T]
to retrieve the corresponding return type.However, for every return statement we have a type error, namely:
Type X is not assignable to type 'never'
whereX
is string or number or boolean, depending on the branch.This is where TypeScript tries to help us narrow down the possibility of problematic states in our program: each return value should be assignable to the type
ReturnTypeByInputType[T]
(as we annotated in the example) whereReturnTypeByInputType[T]
at runtime could end up being either a number, a string, or a boolean.Type safety can only be achieved if we make sure that the return type is assignable to all possible
ReturnTypeByInputType[T]
, i.e. the intersection of number , string, and boolean. And what’s the intersection of these 3 types? It’s exactlynever
as they are incompatible with each other. That’s why we are seeingnever
in the error messages.To work around this, you must use type assertions (or function overloads):
return Math.floor(Math.random() * 10) as ReturnTypeByInputType[T]
return Math.floor(Math.random() * 10) as never
Maybe another more obvious example:
obj[key]
could end up being either a string or a number depending on the value ofkey
at runtime. Therefore, TypeScript added this constraint, i.e., any values we write toobj[key]
must be compatible with both types, string and number, just to be safe. So, it intersects both types and gives usnever
type.How to check for never#
Checking if a type is
never
is harder than it should be.Consider the following code snippet:
Is
Res
true
orfalse
? It might surprise you that the answer is neither:Res
is actuallynever
. In fact,It definitely threw me off the first time I came across this. Ryan Cavanaugh explained this in this issue. It boils down to:
never
is an empty unionnever
again.The only workaround here is to opt out of the implicit distribution and to wrap the type parameter in a tuple:
This is actually straight out of TypeScript’s source code and it would be nice if TypeScript could expose this externally.
In summary#
We covered quite a lot in this blog post:
never
type's definition and purposes.never
is an empty typenever
can come up unexpectedly in type error messages due to implicit type intersectionnever
type.via zhenghao.io
March 26, 2022 at 03:49PM
The text was updated successfully, but these errors were encountered: