DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

When to use `never` and `unknown` in Typescript

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 have to begin with the question, what exactly are types?

Types explained using set theory

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 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. unknown is called the top type for that reason.

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:

https://medium.com/media/053176a7aedd15d6812008a1fb02fb12/href

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 is for things that never happen

Let’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:

https://medium.com/media/e0d87a3378e46b11d8c37672cc9d8fa0/href

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:

https://medium.com/media/eb86420b67d315e1027c6462ac909215/href

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:

https://medium.com/media/6b24031bd871ce4a42a0d0e1b86d43e6/href

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 benefits 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 does not simplify. In that case, we would not be able to access the price property without further type narrowing because the price property would only be listed in one branch of the union.

Use never to prune conditional types

You 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:

https://medium.com/media/8fe2464791aa7d7c9278d6fc5659762d/href

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 or Return. We use never in the else branch of each condition to make that case an error:

https://medium.com/media/462cc073c089bb21970c7dcdf1471aca/href

Conditional pruning is also useful for narrowing union types. Typescript’s libraries include the NonNullable type (source) which removes null and undefined from a union type. The definition looks like this:

https://medium.com/media/545142cf8b6d2371ec38396fc7948b92/href

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:

https://medium.com/media/9f36a600523919c465d797433fe731da/href

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:

https://medium.com/media/589ddfca83199ee38852daa950234919/href

So a type like NonNullable resolves according to these steps:

https://medium.com/media/041de067e436a891a2818ea6a84fa008/href

The result is that given a union type NonNullable produces a potentially narrowed type using never to prune unwanted union branches.

Use unknown for values that could be anything

Any 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:

https://medium.com/media/0de1f86a4235dafee2e2275d437ab7b8/href

You cannot 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 the same way that it does with unknown; so 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 actually, it has not:

https://medium.com/media/6b9cb629f21a1bf0adf40ca82ea197e7/href

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.

How to choose between never, unknown, and any

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.

  • Use never in positions where there will not or should not be a value.
  • Use unknown where there will be a value, but it might have any type.
  • Avoid using any unless you really need an unsafe escape hatch.

In 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!

Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool 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 apps.

Try it for free.


Top comments (0)