Type systems often have types representing the largest and smallest possible sets of values. These are called top and bottom types.
Top types
A top type (symbol: ⊤
) is a type that describes any possible value allowed by the system.
To use our set theory mental model, we could describe this as representing the set { any possible value }
TypeScript provides two of these types: any
and unknown
.
any
You can think of values with an any
type as “playing by the usual JavaScript rules”.
Here’s an illustrative example:
tsTry
letflexible : any = 4flexible = "Download some more ram"flexible =window .document flexible =setTimeout
any
typed values provide none of the safety we typically expect from TypeScript.
tsTry
letflexible : any = 14flexible .it .is .possible .to .access .any .deep .property
It’s important to understand that any
is not necessarily a problem — sometimes
it’s exactly the right type to use for a particular situation.
For example, console.log
:
tsTry
console .log (window ,Promise ,setTimeout , "foo")
We can see here that any
is not always a “bug” or a “problem” — it just indicates maximal flexibility and the absence of a need to type-check before using the value.
unknown
Like any
, unknown can accept any value that is possible to create in JavaScript:
tsTry
letflexible : unknown = 4flexible = "Download some more ram"flexible =window .document flexible =setTimeout
However, unknown
is different from any
in a very important way:
Values with an
unknown
type cannot be used without first applying a type guard
Sometimes people refer to this property of unknown
by describing it as “opaque”.
tsTry
letmyUnknown : unknown = 14'myUnknown' is of type 'unknown'.18046'myUnknown' is of type 'unknown'.myUnknown .it .is .possible .to .access .any .deep .property // This code runs for myUnknown = { all possible values }if (typeofmyUnknown === "string") {// This code runs for myUnknown = { all strings }myUnknown } else if (typeofmyUnknown === "number") {// This code runs for myUnknown = { all numbers }myUnknown } else {myUnknown // this would run for "the leftovers"// myUnknown = { anything except string or numbers }}
TypeScript doesn’t (yet) have the ability to articulate a concept like “anything except a number or a string”, so you may notice that the else { }
block at the end still has myUnknown
as type unknown
.
Practical use of top types
You will run into places where top types come in handy very often. In particular,
if you ever convert a project from JavaScript to TypeScript, it’s very convenient
to be able to incrementally add increasingly strong types. A lot of things will
be any
until you get a chance to give them some attention.
unknown
is great for values received at runtime (e.g., your data layer). By
obligating consumers of these values to perform some light validation before using them,
errors are caught earlier, and can often be surfaced with more context.
Another wise use of unknown
is handling throwables in a catch
block
tsTry
functiondoSomethingRisky () {if (Math .random () > 0.5) return "ok"else if (Math .random () > 0.5) throw newError ("Bad luck!")else throw "Really bad luck"}try {doSomethingRisky ()} catch (e : unknown) {if (e instanceofError ) {e } else if (typeofe === 'string') {e } else {// Last resortconsole .error (e )}}
It’s a good practice to always throw Error
instances, but how sure are you that everything in your node_modules
folder does the same? By typing the argument caught by the catch
block as unknown, you’re effectively forcing yourself to handle these values in a more robust way.
There’s a compiler flag useUnknownInCatchVariables
that helps enforce this across a project, without requiring any explicit type annotation.
tsTry
functiondoSomethingRisky () {if (Math .random () > 0.5) return "ok"else if (Math .random () > 0.5) throw newError ("Bad luck!")else throw "Really bad luck"}try {doSomethingRisky ()} catch (err ) {}
Almost top type: object
The object
type represents the set { all possible values except for primitives }
. Primitive value types in JavaScript are { string, number, boolean, Symbol, null, undefined, BigInt }
It’s important to understand that this is not quite the same concept of the “object types” term used to describe shapes that interface
s can model.
tsTry
letval : object = {status : "ok" }Type 'string' is not assignable to type 'object'.2322Type 'string' is not assignable to type 'object'.= "foo" val Type 'null' is not assignable to type 'object'.2322Type 'null' is not assignable to type 'object'.= null val val = () => "ok"// The type of this value cannot be modeled by an interfaceletresponse :{success : string,data : unknown }| {error : string,code : number }= {success : "ok",data : [] }val =response
Almost top type: {}
The empty object type {}
represents the set { all possible values, except for null and undefined }
tsTry
conststringOrNumber : string | number = 4letnullableString : string | null = nullconstmyObj : {a ?: numberb : string} = {b : "foo" }letval2 : {} = 4val2 = "abc"val2 = newDate ()val2 =stringOrNumber Type 'null' is not assignable to type '{}'.2322Type 'null' is not assignable to type '{}'.= val2 nullableString Type 'number | undefined' is not assignable to type '{}'. Type 'undefined' is not assignable to type '{}'.2322Type 'number | undefined' is not assignable to type '{}'. Type 'undefined' is not assignable to type '{}'.= val2 myObj .a
Based on what we’re seeing here, {} | null | undefined
is technically another top type, since now we’re back to a set of { all possible values }
tsTry
letwithoutUndefined : {} | null = 37letwithUndefined : {} | null | undefined = 38letanUnknown : unknown = "42"Type 'unknown' is not assignable to type '{} | null'.2322Type 'unknown' is not assignable to type '{} | null'.= withoutUndefined anUnknown // ❌withUndefined =anUnknown // ✅
You can use the type {}
in combination with the intersection type operator &
to remove nullability from another type
tsTry
typeNullableStringOrNumber = string | number | null | undefined;typeStringOrNumber =NullableStringOrNumber & {}
Bottom type: never
A bottom type (symbol: ⊥
) is a type that describes no possible value allowed by the system.
To use our set theory mental model, we could describe this as a type representing the set { }
(intentionally empty).
TypeScript provides one bottom type: never
.
At first glance, this may appear to be an extremely abstract and pointless concept, but there’s one use case that should convince you otherwise. Let’s take a look at this scenario below.
Exhaustive conditionals
Let’s consider the following scenario:
tsTry
classCar {drive () {console .log ("vroom")}}classTruck {tow () {console .log ("dragging something")}}typeVehicle =Truck |Car letmyVehicle :Vehicle =obtainRandomVehicle ()// The exhaustive conditionalif (myVehicle instanceofTruck ) {myVehicle .tow () // Truck} else if (myVehicle instanceofCar ) {myVehicle .drive () // Car} else {// NEITHER!constneverValue : never =myVehicle }
Note the assignment in the last else { }
block. This will only work if the type of myVehicle
is type equivalent to never
.
Now, leaving the conditional exactly as-is, let’s add Boat
as a vehicle type:
tsTry
classCar {drive () {console .log ("vroom")}}classTruck {tow () {console .log ("dragging something")}}classBoat {isFloating () {return true}}typeVehicle =Truck |Car |Boat letmyVehicle :Vehicle =obtainRandomVehicle ()// The exhaustive conditionalif (myVehicle instanceofTruck ) {myVehicle .tow () // Truck} else if (myVehicle instanceofCar ) {myVehicle .drive () // Car} else {// NEITHER!constType 'Boat' is not assignable to type 'never'.2322Type 'Boat' is not assignable to type 'never'.: never = neverValue myVehicle }
Effectively, what has happened here is that we have been alerted to the fact that
a new possibility for Vehicle
has been introduced. As a result, we don’t
end up with the type for myVehicle
as a never
in that final else
clause.
I recommend handling this a little more gracefully via an error subclass:
tsTry
classUnreachableError extendsError {constructor(_nvr : never,message : string) {super(message )}}// The exhaustive conditionalif (myVehicle instanceofTruck ) {myVehicle .tow () // Truck} else if (myVehicle instanceofCar ) {myVehicle .drive () // Car} else {// NEITHER!throw newUnreachableError (Argument of type 'Boat' is not assignable to parameter of type 'never'.2345Argument of type 'Boat' is not assignable to parameter of type 'never'., myVehicle `Unexpected vehicle type: ${myVehicle }`)}
Now, one of three things will happen in that final else
block
- We will have handled every case before reaching it, and thus we will never enter the final
else
block - We will catch upstream code changes that need to be handled in this conditional at compile time (e.g., adding the
Boat
case) - If somehow an unexpected value “slip through” and is not caught until we actually run the code, we will get a meaningful error message
Note that this approach works nicely with a switch
statement, when the UnreachableError
is thrown from the default
case clause.
Unit types
Unit types are types that represent a set of exactly one value. An example of this is a literal type
tsTry
letnum : 65 = 65 // represents the set { 65 }
Nothing other than the specific value 65
will work with this type.
In TypeScript, the types null
and undefined
are both unit types.
tsTry
letmyNull : null = nullletmyUndefined : undefined =undefined Type 'undefined' is not assignable to type 'null'.2322Type 'undefined' is not assignable to type 'null'.= myNull undefined Type 'null' is not assignable to type 'undefined'.2322Type 'null' is not assignable to type 'undefined'.= null myUndefined
the void
type is almost a unit type, but it can check against undefined
as well
tsTry
letmyVoid : void = (function() {})()// invoking a void-returning IIFEletmyNull : null = nullletmyUndefined : undefined =undefined myVoid =undefined Type 'null' is not assignable to type 'void'.2322Type 'null' is not assignable to type 'void'.= null myVoid Type 'void' is not assignable to type 'undefined'.2322Type 'void' is not assignable to type 'undefined'.= myUndefined myVoid Type 'void' is not assignable to type 'null'.2322Type 'void' is not assignable to type 'null'.= myNull myVoid