The unknown Type in TypeScript
Exploring TypeScript
Introduced in TypeScript 3.0, the unknown
type exists to provide a type-safe counterpart to the any
type. To understand why this was considered a necessary or desirable addition to TypeScript, we need to revisit any
and why its use can be problematic.
The any
type
The any
type has been in TypeScript since its initial release and represents all possible JavaScript values (making it a top type of the type system).
any
can be problematic because of its flexibility - it allows typing to be circumvented:
// TypeScript
let value: any;
// Any value can be assigned to an 'any' type:
value = true;
value = 0;
value = "Hello world";
value = [];
value = {};
value = null;
value = undefined;
value = Date.now;
value = new Error();
value = Symbol();
// ...
// No check on use:
value * value;
value[1];
value.foo.bar.baz();
new value();
// ...
While convenient, any
undoubtedly increases the risk of errors or unexpected behaviour sneaking into code simply by negating one of the primary benefits of using TypeScript in the first instance - strong typing.
The unknown
type
The unknown
type is less permissive than any
. While any value is assignable an unknown
can only be assigned to itself (or toany
).
Revisiting the previous example:
// TypeScript
let value: unknown;
// Any value can be assigned to an 'unknown' type:
value = true;
value = 0;
value = "Hello world";
value = [];
value = {};
value = null;
value = undefined;
value = Date.now;
value = new Error();
value = Symbol();
// ...
// An 'unknown' type can be assigned to 'any' or 'unknown':
const toAny: any = value;
const toUnknown: unknown = value;
// Assignments to other types will fail:
const toBoolean: boolean = value;
const toString: string = value;
// ...
Narrowing and asserting
Operations cannot be performed on an unknown
type without first narrowing to a more specific type or asserting:
// TypeScript
let value: unknown;
// Operations including the following will error:
value[1];
value.foo.bar.baz();
new value();
// ...
One method of narrowing is with the typeof
operator:
// TypeScript
const value: unknown = 'hello world';
if (typeof value === 'string') {
// Narrowed to a string type, can safely call 'toUpperCase()'
console.log(value.toUpperCase());
}
Types can be asserted using the as
operator:
// TypeScript
const value1: unknown = 'hello world';
const value2: unknown = null;
console.log((value1 as string).toUpperCase()); // Ok
console.log((value2 as string).toUpperCase()); // Error
🔔 Type assertions should be used with caution as they may result in expected run-time issues.
Unions and intersections
In a union, an unknown
absorbs everything else (the exception to this rule is a union with any
).
With an intersection, the converse applies - that is, other types will subsume the unknown
.
// TypeScript
// Unions:
type union1 = unknown | null;
type union2 = unknown | undefined;
type union3 = unknown | boolean;
type union4 = unknown | number;
type union5 = unknown | string;
type union6 = unknown | null | undefined | boolean | number | string;
// ...
type unionN = unknown | any; // any
// Intersections:
type intersection1 = unknown & null;
type intersection2 = unknown & undefined;
type intersection3 = unknown & boolean;
type intersection4 = unknown & number;
type intersection5 = unknown & string;
type intersection6 = unknown & null & string; // never
// ...
type intersectionN = unknown & any; // any
When to use unknown
Wherever possible specify an explicit type and avoid using any
or unknown
. Explicitly specifying types allows you to take advantage of TypeScript’s compile-time checking, provides a more explicit contract (something especially important for library writers to consider), and should result in fewer run-time issues.
Where this is not possible, prefer unknown
over any
as its characteristics require more deliberate and thoughtful use.