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:
tsTryletflexible : any = 4flexible = "Download some more ram"flexible =window .document flexible =setTimeout
any typed values provide none of the safety we typically expect from TypeScript.
tsTryletflexible : 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:
tsTryconsole .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:
tsTryletflexible : 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
unknowntype cannot be used without first applying a type guard
Sometimes people refer to this property of unknown by describing it as “opaque”.
tsTryletmyUnknown : 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
tsTryfunctiondoSomethingRisky () {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.
tsTryfunctiondoSomethingRisky () {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 interfaces can model.
tsTryletval : 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 }
tsTryconststringOrNumber : 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 }
tsTryletwithoutUndefined : {} | 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
tsTrytypeNullableStringOrNumber = 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:
tsTryclassCar {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:
tsTryclassCar {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:
tsTryclassUnreachableError 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
elseblock - We will catch upstream code changes that need to be handled in this conditional at compile time (e.g., adding the
Boatcase) - 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
tsTryletnum : 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.
tsTryletmyNull : 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
tsTryletmyVoid : 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