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 MaybeOption, 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!

👋 Hey, I'm Vlad Mihet
I'm Vlad Mihet, a blogger & Full-Stack Engineer who loves teaching others and helping small businesses develop and improve their technical solutions & digital presence.

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.