Any and Unknown Top Types

Introduction

TypeScript has two so-called top types: any and unknown. any essentially disables the type system, while unknown allows one to be cautious.

A top type is a type compatible with any value of (almost) any type. That is, any values whatsoever can be assigned to any and unknown:

const w: any = 1;
const x: any = { two: 2 };
const y: unknown = "three";
const z: unknown = ["four", 5, { six: 6 }];

Even null and undefined can be assigned to any and unknown:

const h: any = null;
const i: any = undefined;
const j: unknown = null;
const l: unknown = undefined;

While any allows one to do everything with a value, unknown allows nothing. Read on.

any

For these examples, use strict mode or at least set noImplicitAny in tsconfig.json:

{
  "compilerOptions": {
    "strict": true
  }
}

Or at least:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Every value is compatible with any.

Every sub-type except never is compatible with any. never can be assigned to any, but any cannot be assigned to never (because never is a type without any inhabitants).

Any value can be assigned to any:

const x: any = 1;
const y: any = { what: "ever" };

But no value can be assigned to never:

const x: never = 1;
// ~ Type 'number' is not assignable to type 'never'.

Not even any can be assigned to never:

const x: any = 1;
const y: never = x;
// ~ Type 'any' is not assignable to type 'never'

Operations on any

A value of type any allows any operations whatsoever on that value, while a value of type unknown allows no operations unless runtime checks are performed first to serve as guards.

Suppose we start with this:

let yoda: any;

Because we explicitly annotated yoda with any, we disabled type-checking for it. Not even the default type inference will come into play. Therefore, we can read or write to yoda or assume it is an array, or object, or whatever, and the type-checker will remain silent about everything.

log(yoda.name);            (1)
yoda.name = "Master Yoda"; (2)
1 We don’t know if yoda is an object and has a name property.
2 Same as above. Therefore, we don’t know if we can assign a value to it.
yoda = null;                (1)
yoda.power.toString();      (2)
log(yoda.power.toFixed(2)); (3)
1 Assign null to yoda.
2 Then of course it shouldn’t allow reading any properties and invoking methods on them.
3 Same as point 2.

Almost everything we did to the value yoda above would cause runtime errors, but we disabled all type-checking by explicitly annotating yoda to be of type any. And the type checker rests its case and leaves us on our own. And we we’ll get runtime errors, rollbacks, and someone will be held accountable, and will suffer the consequences of their ill type deeds and undoings.

Operations on unknown

let yoda: unknown = { id: 1, name: "Yoda" };

(1)
log(yoda.name);
log(yoda.name.toUpperCase());
// ~ 'yoda' is of type 'unknown'.
1 Unlike the case with any, unknown will not allow reading and writing, or performing operations.
Yoda object with name property on unknown type

But it is possible to apply some type-guards (runtime checks) and thus assure safety. Therefore, unknown is way safer than any as it will only allow operations after proper checks.

/**
 * Checks if `obj` is an object and contains the `name` property.
 */
function hasName(obj: unknown): obj is { name: unknown } {
  if (!obj || typeof obj !== "object")
    return false;

  if (!obj.hasOwnProperty("name"))
    return false;

  return true;
}

if (hasName(yoda)) (1)
  log(yoda.name);  (2)
1 Apply a type-guard.
2 Inside the condition block, the type-checker knows yoda is an object which contains the name property.

Yet, the name property itself is of the type unknown, so we are not allowed to assume we can call methods on it.

Yoda name of type unknown

Again we would need type checks.

if (hasName(yoda) && typeof yoda.name === "string")
  log(yoda.name.toUpperCase());

Now, because we first make sure yoda.name is a string before attempting to invoke a string method on it, the type-checker is satisfied with it.