never
and unknown
in TypeScriptEditor’s note: This article was last updated by Abhinav Anshul on 2 May 2023 to include information about type inference and type annotations, type guards and type assertions, and type narrowing with unknown
.
The never
and unknown
primitive types were introduced in TypeScript v2.0 and v3.0, respectively. These two types represent fundamental and complementary aspects of type theory. TypeScript is carefully designed according to principles of type theory, but it is also a practical language, and its features all have practical uses – including never
and unknown
. To understand those uses, we will be doing a deep dive into TypeScript types.
We’ll cover:
never
typenever
with conditional typesunknown
type in TypeScriptunknown
?any
vs. unknown
vs. never
When you get down to a fundamental definition, a type is a set of possible values, and nothing more. For example, the type string
in TypeScript is the set of all possible strings. The type Date
is the set of all instances of the Date
class (plus all structurally-compatible objects), and the type Iterable<T>
is the set of all objects that implement the iterable interface for the given type of iterated values.
TypeScript is especially faithful to the set-theoretic basis for types; among other features, TypeScript has union and intersection types. A type like string | number
is called a “union” type because it literally is the union of the set of all strings, and the set of all numbers:
The set string | number
contains both the string
and number
sets. Because string | number
contains all string
and all number
values, it is said to be a supertype of string
and of number
:
unknown
is the set of all possible values. Any value can be assigned to a variable of type unknown
. This means that unknown
is a supertype of every other type. For that reason, unknown
is called the top type:
The set unknown
contains all other sets. never
is the empty set. There is no value that can be assigned to variable of type never
. In fact, it is an error for the type of value to resolve to never
because that would be a contradiction. The empty set can fit inside any other set, so never
is a subtype of every other type. That is why never
is called the bottom type:¹
The empty set, never
, exists as a point inside every other set.
The bottom and top types have the useful properties of being the identity element with respect to the union and intersection operations respectively. For any type T
:
T | never ⇒ T T & unknown ⇒ T
This is analogous to the idea that adding zero to a number does not change it, and the same goes for multiplying a number by one. Zero is the identity element for addition, and one is the identity element for multiplication.
A union with the empty set does not add anything, so never
is the identity with respect to unions. An intersection selects the common elements between two sets, but unknown
contains everything so unknown
is the identity with respect to intersections:
never
is the only type that will “factor out” in a type union, which makes it indispensable for certain cases, as we will see in the next section.
never
typeLet’s write some code that makes a network request, but that fails if the request takes too long. We can do that by using Promise.race
to combine a promise for the network response with a promise that rejects after a given length of time. Here is a function to construct that second promise:
function timeout(ms: number): Promise<never> { return new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout elapsed")), ms) ) }
Note the return type: because timeout
never calls resolve
, we could use any type for the promise type parameter, and there would be no contradiction. But the most specific type that will work is never
. (By “most specific”, I mean the type that represents the smallest set of possible values).
Now let’s see timeout
in action:
async function fetchPriceWithTimeout(tickerSymbol: string): Promise<number> { const stock = await Promise.race([ fetchStock(tickerSymbol), // returns `Promise<{ price: number }>` timeout(3000) ]) return stock.price }
This works nicely. But how does the compiler infer the correct return type from that Promise.race
call? race
returns a single promise with the result or failure from the first promise to settle. For purposes of this example, the signature of Promise.race
works like this:
// `Promise.race` specialized for two inputs. // The real signature is more general than this. function race<A, B>(inputs: [Promise<A>, Promise<B>]): Promise<A | B>
The type of the resolved value in the output promise is a union of the resolution types of the inputs. The example above combines fetchStock
with timeout
so the input promise resolution types are { price: number }
and never
, and the resolution type of the output (the type of the variable stock
) should be { price: number } | never
. Because never
is the identity with respect to unions, that type simplifies to { price: number }
, which is what we want.
If we had used any type other than never
as the parameter of the return type in timeout
, things would not have worked out so cleanly. If we had used any
, we would have lost the benefit of type-checking because { price: number } | any
is equivalent to any
.
If we had used unknown
, then the type of stock
would be { price: number } | unknown
, which simplifies to unknown
. In that case, we would not be able to access the price
property without further type narrowing because information about the existence of a price
property would have been lost.
At its core, TypeScript is a type-checking system that allows you to assign a data type to your variable. That data type will ensure that it throws an error whenever the variable is not in the defined boundary of the assigned type.
In the example below, you are enforcing a certain data type to each of the variables. If you try to assign something that is not a valid type, it will throw an error as in the case of city
. city
has been annotated as a type of string, but later a Number type
has been assigned:
let age: number = 9; // number variable let name: string = "Allison";// string variable let isSubscribed: boolean = true; // Boolean variable let city: string = 202; // Type 'number' is not assignable to type 'string'
This behavior of assigning types is called type annotation. But there are trivial cases where TypeScript itself is smart enough to assign suitable data types. For example, in the case below, TypeScript will automatically assign a “string” data type to the name, as name
has been assigned to “Allison”, which is a string data type:
let name = 'Allison' // name is `number` type
Similarly, TypeScript can “infer” the types automatically from the return
statements as well. In the following example, the add
function has a return type of number
. This way of assigning and “inferring” types is called type inference. Sure, you can assign these types manually as well, but it is best to leave those unattended so that TypeScript can infer them automatically:
function add(a:number, b:number){ return a + b }
But in cases where you think you know types better than the inferred types, then you can assign a type yourself called type assertion.
In the example below, TypeScript cannot assign types because it cannot infer from the limited scope of the Person
object. Therefore, you need to assert types here:
var Person = {}; Person.city = 'Kyoto'; // Error: property 'bar' does not exist on `{}` Person.fullname = 'Allison'; // Error: property 'bas' does not exist on `{}`
There are several ways you can define types here, such as interface
or creating types object. In the following code, the Person
object is now mapped to the IPerson
interface using the as
keyword:
interface IPerson { city: string; fullname: string; } var Person = {} as IPerson Person.city = 'Kyoto'; Person.fullname = 'Allison';
This way, Person
object has a proper type-safety using type assertion.
Type guards is a trick that uses native JavaScript features such as typeof
, instanceof
, in
, etc., to narrow down the type with the help of conditional blocks. These conditional blocks can be either if/else or switch statements.
In the following example, you can use type guards to check if a value provided to the test
function is of type string, then only check its length. Otherwise, it logs with a message. This is how you can guard your type using if/else
conditional blocks, and perform a set of tasks based on its type:
function test(n: string | number){ if(typeof n === string){ console.log(n.length) } else { console.log("n is a number") } }
Here’s another example, where the instanceof
method is being used to type guard logic inside the function based on class Person
and Animal
:
class Person { height : 6 } class Animal { height : 3 } function calculate(arg : Person | Animal){ if(arg instanceof Person){ console.log(`Person height is ${arg.height}`) } if(arg instanceof Animal){ console.log(`Animal is ${arg.height}}`) } } calculate(new Person()) //or //calculate(new Animal())
never
with conditional typesYou will often see never
used in conditional types to prune unwanted cases. For example, these conditional types extract the argument and return types from a function type:
type Arguments<T> = T extends (...args: infer A) => any ? A : never type Return<T> = T extends (...args: any[]) => infer R ? R : never function time<F extends Function>(fn: F, ...args: Arguments<F>): Return<F> { console.time() const result = fn(...args) console.timeEnd() return result }
If T
is a function type, then the compiler infers its argument types or return type. But if T
is not a function type, then there is no sensible result for Arguments<T>
or Return<T>
. We use never
in the else branch of each condition to make that case an error:
// Error: Type '3' is not assignable to type 'never'. const x: Return<"not a function type"> = 3
Conditional pruning is also useful for narrowing union types. TypeScript’s libraries include the NonNullable<T>
type (source), which removes null
and undefined
from a union type. The definition looks like this:
type NonNullable<T> = T extends null | undefined ? never : T;
This works because conditional types distribute over type unions. Given any type of the form T extends U ? X : Y
when a union type is substituted for T
, the type expands to distribute the condition to each branch of that union type:
// if T = A | B T extends U ? X : T == (A extends U ? X : A) | (B extends U ? X : B)
In each union branch, every occurrence of T
is replaced by one constituent from the substituted union type. This also applies if T
appears in the true case instead of the false case, or occurs inside of a larger type expression:
// if T = A | B T extends U ? SomeGeneric<T> : Y == (A extends U ? SomeGeneric<A> : Y) | (B extends U ? SomeGeneric<B> : Y)
So a type like NonNullable<string | null>
resolves according to these steps:
NonNullable<string | null> // The conditional distributes over each branch in `string | null`. == (string extends null | undefined ? never : string) | (null extends null | undefined ? never : null) // The conditional in each union branch is resolved. == string | never // `never` factors out of the resulting union type. == string
The result is that, given a union type, NonNullable<T>
produces a potentially narrowed type using never
to prune unwanted union branches.
unknown
type in TypeScriptAny value can be assigned to a variable of type unknown
. So use unknown
when a value might have any type, or when it is not convenient to use a more specific type. For example, a pretty-printing function should be able to accept any type of value:
function prettyPrint(x: unknown): string { if (Array.isArray(x)) { return "[" + x.map(prettyPrint).join(", ") + "]" } if (typeof x === "string") { return `"${x}"` } if (typeof x === "number") { return String(x) } return "etc." }
You can’t do much with an unknown
value directly. But you can use type guards to narrow the type and get accurate type-checking for blocks of code operating on narrowed types.
Prior to TypeScript 3.0, the best way to write prettyPrint
would have been to use any
for the type of x
. Type narrowing works with any
mostly the same way that it does with unknown
; the compiler can check that we used map
and join
correctly in the case where x
is narrowed to an array type regardless of whether we use any
or unknown
. But using unknown
will save us if we make a mistake where we think that the type has been narrowed, but it actually has not:
import isArray from "isarray" function prettyPrint(x: any): string { if (isArray(x)) { // whoops - this `isArray` is not a type guard! return "[" + x.mop(prettyPrint).join(", ") + "]" } /* snip */ return "etc." }
The isArray
package does not include type definitions to turn the isArray
function into a type guard. But we might use isArray
without realizing that detail.
Because isArray
is not a type guard, and we used any
for the type of x
, the type of x
remains any
in the if
body. As a result, the compiler does not catch the typo in this version of prettyPrint
. If the type of x
were unknown
, we would have gotten this error instead:
Object is of type ‘unknown’.
In addition, using any
lets you cheat by performing operations that are not necessarily safe. unknown
keeps you honest.
unknown
?As the name suggests, type narrowing is used to narrow down your types that were declared using unknown
. When a value is declared using unknown
, TypeScript has no idea about its type, which means you can’t use any of its available methods until you explicitly “narrow” down its type to something specific.
For example, you know that length
is a property that is available on the string
data type. You can check for the length using this:
hello.string; // 6
Now, if you assign this to unknown
type, the following code throws an error as it is not sure if .length
property exists on a type of unknown
:
function checkLength(value : unknown){ console.log(value.length) // Error } checkLength('hello')
To solve this, you can narrow down the value
type and check if the value is of type string
, then only proceed to check for .length
, as length is a property that is available on type string:
function checkLength(value: unknown){ if(typeof value === 'string'){ console.log(value.length) // 6 } } checkLength('hello')
any
vs. unknown
vs. never
The type of x
in prettyPrint
and the promise type parameter in the return type of timeout
are both cases where a value could have any type. The difference is that, in timeout
, the promise resolution value could trivially have any type because it will never exist:
never
in positions where there will not or should not be a valueunknown
where there will be a value, but it might have any typeany
unless you really need an unsafe escape hatchIn general, use the most specific type that will work. never
is the most specific type because there is no set smaller than the empty set. unknown
is the least specific type because it contains all possible values. any
is not a set, and it undermines type-checking, so try to pretend that it does not exist when you can.
[1]: ^ There is a crucial distinction between never
and null
: the type null
is actually a unit type, meaning that it contains exactly one value, the value null
. Some languages treat null
as though it is a subtype of every other type, in which case it is effectively a bottom type. (This includes TypeScript if it is not configured with strict checking options.) But that leads to contradictions because, for example, null
is not actually present in the set of all strings. So please use the --strictNullChecks
compiler option to get contradiction-free treatment of null
!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
4 Replies to "When to use <code>never</code> and <code>unknown</code> in TypeScript"
Extremely well explained, thanks a lot!
Double Extremely well explained! Thanks a ton!
What a pleasure! A great combination of theory + examples
This is the best blog about typescript I have read