Derek Lawless

There is always sunshine / Far above the grey sky

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.

© 2022 Derek Lawless. Built with Gatsby