Statically Type Unknown Values with TypeScript's unknown Type

Marius Schulz
InstructorMarius Schulz
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 4 years ago

In this lesson, we'll go over TypeScript's unknown type. The unknown type is the type-safe counterpart of the any type. Both unknown and any are so-called top types (also known as universal supertypes) of the type system. This means that every type in TypeScript is assignable to both unknown and any.

The main difference between the two types is that unknown is much less permissive than any: We have to do some form of checking before performing most operations on values of type unknown, whereas we don't have to do any checks before performing operations on values of type any.

Another difference is that any is assignable to every type, whereas unknown is only assignable to any and unknown itself. To assign unknown to any other types, we have to narrow it to a more specific type first.

Additional Reading

Instructor: [0:00] Let's say we have a variable called value that is of type any. In TypeScript, all types are assignable to the any type. This means our value variable can hold arbitrary values. We can assign primitive values, such as Booleans, numbers or strings, but we can also assign more complex values, such as objects, arrays or even functions.

[0:27] Of course, we can also assign the values null and undefined. When we are using the any type, we can essentially do anything. For example, we can access arbitrary properties, and we can call arbitrary methods. However, these operations can fail at runtime, depending on the value that we hold in our variable.

[0:49] Let me simplify this example a bit. Let's say that we want to take a string, uppercase it and then print it to the console. What we can do is we can assign this value to a variable called uppercase text, and then we want to say console.log uppercase text.

[1:08] Let's go ahead and open the terminal. We're going to compile our TypeScript project. Then we're going to execute the compiled JavaScript file in Node. As you can see, we're getting the infamous error "Cannot read property of undefined." The problem with our code is that we're using the any type. When we're using any, we don't get any protections from TypeScript.

[1:33] Also, notice how the any type is spreading in our code. Value is of type any. Therefore, the toUpperCase() Method is also of type any. Therefore, its return type is of type any. Therefore, our uppercase text variable is of type any. We've lost useful type coverage in most of our code.

[1:54] Let's now look at a safer approach that uses the unknown type. Unknown is another universal supertype in TypeScript's type system. This means that we can assign all types to unknown, like we can assign all types to any. The big difference between any and unknown is that it's essentially flipping the default from allowing everything to allowing almost nothing.

[2:17] In our example, TypeScript is telling us that the value variable is of type unknown, so we cannot simply call the toUpperCase() Method on it. First, we have to narrow the unknown type to something more specific, for example, the type string.

[2:34] One of the ways that we can do this kind of type narrowing is using the typeof operator. What we want to check is whether our value is actually a string before we try to use it as a string. Now, a bunch of things have happened. First of all, you can see that the TypeError went away.

[2:53] This is because, out here, value is typed to be of type unknown, but within the if statement, we've narrowed our value variable to be of type string. We can now call string methods on it, for example, toUppercase(). Also note that, now that we know that there's a toUppercase() Method, we also know its return type, which is string, so uppercaseText is now typed to be a string.

[3:20] Go ahead and run our code again. As you can see, you see nothing. We no longer get an error, but we also don't get any output. This is expected, though. We only want to log our uppercase string if we actually have a string. If I go back in and I swap lines two and three, value now contains a string.

[3:45] If we rerun this one more time, now we get our uppercase string. Whenever you're unsure whether you should be using any or unknown, I would encourage you to bias towards unknown. It is the safer default, and TypeScript helps you put the correct checks in place rather than make assumptions about your values.

[4:08] Let me show you another example where we can use the unknown type. Here, we have a range() function. It takes from and to as parameters, and it returns the range of numbers in between those bounds. If I run this code, we'll get the range from zero to five, excluding five itself.

[4:27] If you're only using this function within your own TypeScript project and you have good type coverage everywhere, you can stop right here and you don't have to do what I'm about to show you. However, if you're exporting this function in, say, a JavaScript library published to npm, there's a good chance you will have JavaScript consumers of this code.

[4:45] Some of them might not see your type annotations, so we might want to put some additional input validation in place. The unknown type can help us with that. Let's add some validation code to verify that our two parameters are actually two numbers. TypeScript can help us do this if we type our parameters as unknown instead of using the type number.

[5:08] The unknown type is our way of saying that we don't know the shape of the values we will be receiving. Within the body of our function, TypeScript is now giving us a bunch of TypeErrors. That's because we're using parameters of type unknown as if they were numbers without checking that they actually are numbers. Let's put these checks in place.

[5:28] If either the from param or the to param is something else than a number, we want to throw an error and let the caller know that they're using the range() function incorrectly. TypeScript is clever. It understands that from and to must be numbers. If I hover over from in line seven, you can see that it has type number.

[5:50] The same is true for two. If either parameter wasn't a number, we would have thrown an error in line three and we would have left the function. If we make it parse the if statement, from and to must be numbers. This is great. Our code is now more defensive than it was before, but we've also lost something in the process.

[6:12] If I hover over the range() function call below, you can see that the from and to parameters now show up as unknown in the function signature, because those are the types that we have assigned. This is not great, because we want to communicate to our TypeScript callers that from and to are meant to be numbers.

[6:29] We can fix this by adding an overload signature to our function. I'm going to copy-paste the function signature, but instead of using the type unknown, we are going to use the type number. With this overload signature in place, callers of our function can now see that the from and to parameters are meant to be numbers.

[6:50] Within our range() function, though, we are treating from and to as unknown values, and we first have to convince ourselves that we're, in fact, dealing with numbers. If we accidentally forget to write one of these checks, TypeScript will tell us that we are working with an unknown value. This is especially helpful if we're trying to write defensive code.