Saturday, April 03, 2021

any, unknown and never types in Typescript

This post will be a quick overview of three interesting types in Typescript: any, unknown, and never with the aim of quickly explaining what they are, and when to use them. It is part of the Introduction to Advance Types in TypeScript series.  

To start with, it is a good mental model to view Types from the perspective of set theory. This idea is fully explored in Union and Intersection Types in TypeScript, but for a quick summary, the idea is simple. When types are created, see it as similar to defining a Set. And what do sets contain? they contain objects. The next thing is to see values as objects that belongs to a set. A value defined to be part of a set, would not be allowed in a different set which it does not belong in or overlap with. 

For example, the type string is the set of all possible strings, which is an infinite amount, while the type boolean is the set of all possible boolean values, which in this case is finite and is just true and false. That is the simple idea.

Now let's explore any, unknown and never types in TypeScript

any

any is a type that can contain all of the values in Javascript. So if you have a value, maybe from a Javascript library that does not have type annotations, or it is a hassle to define the type, then ascribing it to any will satisfy the type checker. This is because if any is seen as a set, it is the superset that can contain all values.

In type theory a type like any is referred to as the Top Type (universal type, or universal supertype) as values from all types can be assigned to it, meaning it is a universal type of all types. This can be seen in the code snippets below

let value: any;

let boolValue: boolean = true
let numValue: number = 43
let strValue: string = "Hello world"

value = boolValue
value = numValue
value = strValue
In the above, values of type boolean, number and string are assigned to any. Any native type within Javascript or custom type will also work.

The repercussion of the above, is that we lose type safety. Once a value is assigned any the typescript compiler can no longer ascertain the original type and what operations are allowed or not. Hence you no longer get the red squiggly lines or the compilation error, but then the error should have been caught during development would only show up at Runtime. 

For example:
let value: any;

let boolValue: boolean = true

value = boolValue
value.charCodeAt(0)
In the code above, value is assigned to any and then a boolean value is assigned to it. After that, a method that should only be called on a string value is used on it. This is wrong, but the compiler won't be able to point this out. This is not desirable. 

When to use any? Probably when prototyping and want to get an implementation working. any should probably not be used in production, as all bets are off the table when any is used. Littering any within a codebase is more or less the same as just using Javascript. So you use Typescript, get none of the type-safety benefits but end up with arguably uglier code with all the any annotations.

Instead of any, there is an alternative type that should generally be used instead of any. That is what is looked at next.

unknown

unknown is also another top type in Typescript. Meaning values of all types can be ascribed to the unknown type.

let value: unknown;

let boolValue: boolean = true
let numValue: number = 43
let strValue: string = "Hello world"

value = boolValue
value = numValue
value = strValue
The main difference between unknown and any is that, with unknown, the compiler now no longer allows any method calls. Any attempt to do so will lead to a compilation error. For example this:
let value: unknown;

let boolValue: boolean = true

value = boolValue
// compile error
value.charCodeAt(0)
will lead to a compilation error, which is a pragmatic thing to do. If the compiler cannot ascertain valid operations, why allow any operations at all? When there is the risk of it being invalid and blowing up at runtime?

The question then is, when unknown is used, how can one do anything with it? 

The Typescript compiler only allows operations on unknown types after certain checks have been performed that the value being operated on, is indeed of a type that supports the operation. For example.

let value: unknown;

let boolValue: boolean = true

value = boolValue

if (typeof value === "string") {
  value.charCodeAt(0)
}

This means in other to use a value of type unknown, you effectively need to do type checking before the operation is allowed. This process is broadly called Type Narrowing, using Type guards, and is related to Typescript's flow-sensitive features. These ideas are also covered in a little bit more details in Union and Intersection Types in TypeScript

So when to use unknown? In the cases where any would normally be used, but still, desire the compiler to provide type safety as the compiler will enforce that you manually check the value is of a particular type before allowing operations like method calls to be made.

never

if any is the Top type, never is the Bottom type. What does this mean? Well if the Top type is a type that can contain values of all the types, the Bottom type is a type (read sets) that does not contain any value of any type. Essentially Bottom type is an empty set.

If we have a type that contains nothing, what can we do with such?

never is used to model situation where an operation can never return a value. Such situations include a function that can never return a value. For example a function that continually prints out the timestamp:

function neverReturns(): never {
  while(true) {
    console.log(Date.now())
  }
}
How to type this function? it won't return any value. Hence why the type never can be ascribed to it. 

never, can also be used when dealing with union types. In such situations the ability to model that no value should exist, can be put to use to ensure type safety. The following code illustrate this.  

Given we have a value of union type number | boolean, we can write code, using type narrowing that ensures we take care of when the value is actually number or boolean:
let value: number | boolean = 1 

function process(value: number | boolean) {
  if (typeof value === "number") {
    // TODO operate on value as number
  } else {
    // TODO operate on value as boolean
  }
}
The problem with this, is that in the future if the union type for value is extended to include another type, this new type won't be accounted for in the process function. Hence if a value of boolean is passed to the function, it will only blow up at runtime, and there is no way the compiler can warn us of this. To fix this, we can use never type. The trick is to include a branch that takes the passed-in value and assigns it to another value of type never. This should never happen, if it does, it means there is a value that is not being handled in the branches. An attempt to now assign such a value to type never will lead to compile error. This is how we can use the never to get some more type-safety. This process is called exhaustive type checking, as it is a mechanism that allows us the guarantee at compile time that all possible types in a union type are accounted for.

These are not the only scenarios where never type can be used. There are other interesting and perhaps advanced use cases. But the general idea is that the never type never contains any value. And these characteristics can prove to be useful.

This post is part of a series. See Introduction to Advanced Types in TypeScript for more posts in the series.


I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.

2 comments:

Zti said...

> unknown is used to model situation where an operation can never return a value.
I believe this should be 'never' instead of 'unknown'

dade said...

Indeed. Thanks for spotting this. I have updated the post