All Articles

TypeScript unknown vs any types

TypeScript 3.0 introduced a new unknown type which is the type-safe counterpart of the any type.

The main difference between unknown and any 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.

Reference

The any Type

Let’s first look at the any type so that we can better understand the motivation behind introducing the unknown type.

The any type has been in TypeScript since the first release in 2012. It represents all possible JavaScript values — primitives, objects, arrays, functions, errors, symbols, what have you.

In TypeScript, every type is assignable to any. This makes any a top type (also known as a universal supertype) of the type system.

Here are a few examples of values that we can assign to a variable of type any:

let value: any

value = true // OK
value = 42 // OK
value = 'Hello World' // OK
value = [] // OK
value = {} // OK
value = Math.random // OK
value = null // OK
value = undefined // OK
value = new TypeError() // OK
value = Symbol('type') // OK

The any type is essentially an escape hatch from the type system. As developers, this gives us a ton of freedom: TypeScript lets us perform any operation we want on values of type any without having to perform any kind of checking beforehand.

In the above example, the value variable is typed as any. Because of that, TypeScript considers all of the following operations to be type-correct:

let value: any

value.foo.bar // OK
value.trim() // OK
value() // OK
new value() // OK
value[0][1] // OK

In many cases, this is too permissive. Using the any type, it’s easy to write code that is type-correct, but problematic at runtime. We don’t get a lot of protection from TypeScript if we’re opting to use any.

What if there were a top type that was safe by default? This is where unknown comes into play.

The unknown Type

Just like all types are assignable to any, all types are assignable to unknown. This makes unknown another top type of TypeScript’s type system (the other one being any).

Here’s the same list of assignment examples we saw before, this time using a variable typed as unknown:

let value: unknown

value = true // OK
value = 42 // OK
value = 'Hello World' // OK
value = [] // OK
value = {} // OK
value = Math.random // OK
value = null // OK
value = undefined // OK
value = new TypeError() // OK
value = Symbol('type') // OK

All assignments to the value variable are considered type-correct.

What happens though when we try to assign a value of type unknown to variables of other types?

let value: unknown

let value1: unknown = value // OK
let value2: any = value // OK
let value3: boolean = value // Error
let value4: number = value // Error
let value5: string = value // Error
let value6: object = value // Error
let value7: any[] = value // Error
let value8: Function = value // Error

The unknown type is only assignable to the any type and the unknown type itself. Intuitively, this makes sense: only a container that is capable of holding values of arbitrary types can hold a value of type unknown; after all, we don’t know anything about what kind of value is stored in value.

Let’s now see what happens when we try to perform operations on values of type unknown. Here are the same operations we’ve looked at before:

let value: unknown

value.foo.bar // Error
value.trim() // Error
value() // Error
new value() // Error
value[0][1] // Error

Narrowing the unknown Type

We can narrow the unknown type to a more specific type in different ways, including the typeof operator, the instanceof operator, and custom type guard functions.

The following example illustrates how value has a more specific type within the two if statement branches:

function stringifyForLogging(value: unknown): string {
  if (typeof value === 'function') {
    // Within this branch, `value` has type `Function`,
    // so we can access the function's `name` property
    const functionName = value.name || '(anonymous)'
    return `[function ${functionName}]`
  }

  if (value instanceof Date) {
    // Within this branch, `value` has type `Date`,
    // so we can call the `toISOString` method
    return value.toISOString()
  }

  return String(value)
}

In addition to using the typeof or instanceof operators, we can also narrow the unknown type using a custom type guard function:

/**
 * A custom type guard function that determines whether
 * `value` is an array that only contains numbers.
 */
function isNumberArray(value: unknown): value is number[] {
  return (
    Array.isArray(value) && value.every(element => typeof element === 'number')
  )
}

const unknownValue: unknown = [15, 23, 8, 4, 42, 16]

if (isNumberArray(unknownValue)) {
  // Within this branch, `unknownValue` has type `number[]`,
  // so we can spread the numbers as arguments to `Math.max`
  const max = Math.max(...unknownValue)
  console.log(max)
}

Notice how unknownValue has type number[] within the if statement branch although it is declared to be of type unknown.

Using Type Assertions with unknown

If you want to force the compiler to trust you that a value of type unknown is of a given type, you can use a type assertion like this:

const value: unknown = 'Hello World'
const someString: string = value as string
const otherString = someString.toUpperCase() // "HELLO WORLD"

Be aware that TypeScript is not performing any special checks to make sure the type assertion is actually valid. The type checker assumes that you know better and trusts that whatever type you’re using in your type assertion is correct.

This can easily lead to an error being thrown at runtime if you make a mistake and specify an incorrect type:

const value: unknown = 42
const someString: string = value as string
const otherString = someString.toUpperCase() // BOOM

The value variable holds a number, but we’re pretending it’s a string using the type assertion value as string. Be careful with type assertions!

Published 13 Mar 2021

Recent Posts

Migrate Angular to ESLint

I'll show you how to migrate from TSLint to ESLint and using husky to run lint every time you try to make a commit

Convert Promise to Observable

You'll need those tips one day if you get used to RxJS 🤣


Follow @tuantrungvo on Twitter for more!