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