Introduction
Not so long ago, in version 3.0, TypeScript introduced a new `unknown` type to its toolkit, but what does this type do, and more specifically, when should we use it?
This article will look at this “new” type to better understand its goal.
We’ll also consistently compare it to its sibling, the any
type; if you are not familiar with it yet, check out this article, where we have discussed it in more detail.
What is the unknown type?
In Typescript, any value can be assigned to the unknown
type, but without a type assertion, unknown
can’t be assigned to anything but itself and the any
type. Similarly, no operations on a value with its type set as unknown
are allowed without first asserting or restricting it to a more precise type.
We can assign any value to a variable of the unknown
type the same way we can with a variable of the any
type. Unlike in any
‘s case, we cannot access properties on variables of the unknown
type, nor call or construct them.
Additionally, unknown
values can only be assigned to variables of the types any
and unknown
:
let val: unknown;
val = true; // Fine
val = 42; // Fine
val = "hey!"; // Fine
val = []; // Fine
val = {}; // Fine
val = Math.random; // Fine
val = null; // Fine
val = undefined; // Fine
val = () => { console.log('Hey again!'); }; // Fine
let val: any;
val = true; // Fine
val = 42; // Fine
val = "hey!"; // Fine
val = []; // Fine
val = {}; // Fine
val = Math.random; // Fine
val = null; // Fine
val = undefined; // Fine
val = () => { console.log('Hey again!'); }; // Fine
let val: unknown;
const val1: unknown = val; // Fine
const val2: any = val; // Fine
const val3: boolean = val; // Will throw error
const val4: number = val; // Will throw error
const val5: string = val; // Will throw error
const val6: Record<string, any> = val; // Will throw error
const val7: any[] = val; // Will throw error
const val8: (...args: any[]) => void = val; // Will throw error
Making the unknown type more specific
We can narrow down the possible outcomes of an unknown
type value.
Let’s take a look at the following example:
const isNumbersArray = (val: unknown): val is number[] => (
Array.isArray(val) && val.every((element) => typeof element === 'number')
);
const unknownValue: unknown = [12, 2, 8, 17, 14];
if (isNumbersArray(unknownValue)) {
const sum = unknownValue.reduce((accumulator, currentElement) => (accumulator + currentElement) , 0);
console.log(sum);
}
Previously, the unknownValue
‘s type was unknown
; however, after calling the isNumbersArray
with the unknownValue
as its argument, we’ve concluded that the type of the unknownValue
is, in fact, that of an array of numbers.
When would we need to use the unknown type?
One particular scenario where we might want to use the unknown
type is where we need to grab something from local storage.
All localStorage
API’s items are being serialized before storage. However, our use case includes the value of the retrieved item after deserialization.
By making use of the unknown
type, we’ll be able to type out the deserialized item’s type rather than simply using the any
annotation.
type ResultType =
| { success: true; value: unknown }
| { success: false; error: Error };
const retrieveItemFromLocalStorage = (key: string): ResultType => {
const item = localStorage.getItem(key);
if (!item) {
// The item does not exist, thus return an error
return {
success: false,
error: new Error(`Item with key "${key}" does not exist`),
};
}
let value: unknown;
try {
value = JSON.parse(item);
} catch (error) {
// The item is not a valid JSON value, thus return an error
return {
success: false,
error,
};
}
// Everything's fine, thus return a successful result
return {
success: true,
value,
};
}
The ResultType
is a tagged union type (also known as a discriminated union type). You might come across Maybe
, Option
, or Optional
, which are its equivalent in other languages. We use ResultType
to model the successful or unsuccessful outcomes of the operation cleanly.
And here’s an example of how we would use our retrieveItemFromLocalStorage
function:
const result = retrieveItemFromLocalStorage("cached-blogs");
if (result.success) {
// We've narrowed the `success` property to `true`,
// so we can access the `value` property
const cachedBlogs: unknown = result.value;
if (isArrayOfBlogs(cachedBlogs)) {
// We've narrowed the `unknown` type to `Array<Blog>`,
// so we can safely use our cached `cachedBlogs` array as we would
console.log('Cached blogs:', cachedBlogs);
}
} else {
// We've narrowed the `success` property to `false`,
// so we can access the `error` property
console.error(result.error);
}
Summary
I hope you’ve enjoyed the read and have gotten a bit more insight into what the unknown
type is what makes it different from the any
type, and when we are usually using it in real projects.
Feel free to leave your thoughts in the comments section below.
Cheers!
💬 Leave a comment