Types describe sets of allowed values
Let’s imagine that types describe a set of allowed values that a value might be.
For example:
ts
const x: boolean
x could be either item from the following set {true, false}
. Let’s look at another example:
ts
const y: number
y could be any number. If we wanted to get technical and express this in terms of set builder notation, this would be {y | y is a number}
1
Let’s look at a few more, just for completeness:
tsTry
leta : 5 | 6 | 7 // anything in { 5, 6, 7 }letb : null // anything in { null }letc : {favoriteFruit ?: "pineapple" // { "pineapple", undefined }}
Hopefully this makes sense. Now we are ready to continue…
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 {x| x could be anything }
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 type checking validation.
unknown
Like any
, unknown can accept any value:
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
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| anything }if (typeofmyUnknown === "string") {// This code runs for { myUnknown| all strings }console .log (myUnknown , "is a string")} else if (typeofmyUnknown === "number") {// This code runs for { myUnknown| all numbers }console .log (myUnknown , "is a number")} else {// this would run for "the leftovers"// { myUnknown| anything except string or numbers }}
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.
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 “any value from the following 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 }
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.
-
Technically in JS or TS this would be
↩{ y| -Number.MAX_VALUE <= y <= Number.MAX_VALUE }
, but if you know enough to ask, you probably don’t need this footnote…!