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
.
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.
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
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
.
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!