TypeScript — Make types “real”, the type guards
This article is part of the collection “TypeScript Essentials”,
Chapter six.
The previous chapters showed us all the principles we need to master in order to write maintainable and powerful types.
However, TypeScript is often resumed as “only being a static type checker”, meaning that, at runtime, all the gains of types are a loss.
In this chapter, we will see together that this allegation is totally untrue.
The lack of runtime type checking/assertions
One historical blame on TypeScript is the lack of “runtime checking”.
The answer to this “runtime type checking” topic from the TypeScript core team is clear:
Let’s look at the TypeScript design goals regarding runtime type checking:
We will see that this is not a fatality, because TypeScript is more powerful than you thought and some developers of the community are very crafty.
Here comes Type Guards
First, what does mean:
“making types real”
As you noticed, TypeScript does a static analysis of your code to infer types (“guess” types based on function calls, variables tracking, basically the AST),
which make it impossible to, for example, create types based on the script running behavior (for example: “if this object have this dynamic property, then this is a MyType
type).
This “gap” between what we call “runtime” and “static analysis” can be filled using TypeScript Type Guards.
TypeScript provides 2 way to do Type Guards:
- by using
instanceof
andtypeof
JavaScript operators - by creating “User-Defined Type Guards”: defining your own type assertions using Functions,
“instanceof” and “typeof” Type Guards
— — —
typeof
Considering the following formatMoney
function that accepts a string
or a number
:
function formatMoney(amount: string | number): string {
let value = amount; // value type is number or string
if (typeof amount === "string") {
value = parseInt(amount, 10); // amount type is string
}
return value + " $"; // value type is number
}
Let’s take a look at what TypeScript documentation says about
the typeof
type guard:
These
typeof
type guards are recognised in two different forms:typeof v === "typename"
andtypeof v !== "typename"
, where"typename"
must be"number"
,"string"
,"boolean"
, or"symbol"
.
While TypeScript won’t stop you from comparing to other strings, the language won’t recognise those expressions as type guards.
Okay, TypeScript recognize certain use of typeof
as type guard, but what does it mean?
It means that when having a type guard:
TypeScript and JavaScript runtime are tied to the same behaviour.
Let’s take a look at:
if (typeof amount === "string") {
value = parseInt(amount, 10); // amount type is string
}
Inside the if
statement, TypeScript will assume that amount
cannot be anything else than a string
, which is true also at the runtime thanks to typeof
JavaScript operator.
However, when looking at:
function formatMoney(amount: string | number): string {
TypeScript will try to prevent you to call formatMoney
with anything else than a string
or a number
, however, nothing prevents you to do:
formatMoney({ myObject: 1} as any)
The cool thing here is that the typeof amount === "string"
JavaScript operator usage will prevent the parseInt
to be called with an object.
— — —
instanceof
The instanceof
operator is also a type guard recognized by TypeScript, however, this one applies to function constructor (aka classes).
Here’s how is defined the behavior of the instanceof
type guard in the TypeScript definition:
The right side of the
instanceof
needs to be a constructor function, and TypeScript will narrow down to:1. the type of the function’s
prototype
property if its type is notany
2. the union of types returned by that type’s construct signatures
in that order.
User-Defined Type Guards: write your own Type Guard
As seen in the previous chapter, “real-world usage” of TypeScript is not restricted to scalar types (string
, boolean
, number
, etc…).
Real-world applications mainly deal with complex object
or custom types.
This is when “User-Defined Type Guards” help us.
Let’s imagine an application with the following types.
⤵️
A User
can be either free or premium.
A PremiumUser
is slightly different from a User
object, it has always present properties, specific to a premium user: billing
, customisations
and primary_billing
.
This makes sense because no user can become premium without giving a billing record.
Let’s see now what does it mean for our application:
For us, user.plan
is “premium”
means that user if a PremiumUser
object.
Since we are not using Discriminated Unions, TypeScript doesn’t know that type could be inferred.
In order to indicate this type inference to TypeScript, we are gonna define a User-Defined Type Guards:
Let’s took a close look to our first type guard:
function isPremiumUser(user: User): user is PremiumUser {
return user.plan === 'premium';
}
- the type guard function argument type, like for overloads, should be as open as possible (in order to be used as much as possible)
Hereuser
can be any kind ofUser
. - we can notice a new
is
operator, called type predicate.
Basically, usingin
indicate to TypeScript that it can trust us on the return type (you can see it as aas
for function return type).
— — —
Tip: Forget switch
for redux reducers, use if
with type-guards 🎉
Community Runtime checking libraries
Now that you know how to build your own Type Guards, let’s take a look at what the TypeScript community accomplished with them.
Spicery
Manuel Alabor introduced this library in his interesting article entitled
“Pattern Matching with TypeScript”.
In this article, Manuel showed that Pattern Matching is achievable with TypeScript using his Spicery library, based on User-Defined Type Guards.
Let’s take a look at this library.
Spicery tagline is: “Runtime type safety for JSON/untyped data”.
In short, Spicery aim to provide runtime type safety for external data, which is a good point because, in a SPA, most of the untyped data comes from external APIs (XHR calls).
Since this library is no longer maintained, we will not go further on it, however I highly advise you to read Manuel Alabor article:
io-ts
Introduced in the perfectly named “Typescript and validations at runtime boundaries” article @lorefnon, io-ts is an active library that aim to solve the same problem as Spicery:
TypeScript compatible runtime type system for IO decoding/encoding
But how t.interface
infer the argument object type?
Let’s look at t.interface
definition:
The powerful thing with this library is that t.interface()
will return an InterfaceType
class of type InterfaceType
that will propagate the type of the given object to the props
property and will also give you a Type Guard method called is()
having a default implementation of a type guard based of object comparison.
io-ts enable also to alias/rename the inferred type by doing the following:
interface IPerson extends t.TypeOf<typeof Person> {}
// same as
interface IPerson {
name: string
age: number
}
— — — —
While this io-ts might look “overkill”, if you’re dealing with data or complex SPA, you might want to make your types “real” and io-ts is the best way to reach this.
ts-runtime
ts-runtime take another approach to runtime type assertions by using transpilation.
Sharing the same goals as io-ts and Spicery, ts-runtime will transform your code at compilation, just like TypeScript or Babel already do.
From the documentation:
This function simply creates a number from a string.
function getNumberFromString(str: string): number {
return Number(str);
}
The code below is the result, with runtime type checks inserted.
function getNumberFromString(str) {
let _strType = t.string();
const _returnType = t.return(t.number());
t.param("str", _strType).assert(str);
return _returnType.assert(Number(str));
}
— — —
While still experimental, io-ts might be a good solution if you want to keep your existing code less impacted as possible by runtime type assertions.
Appealed? you should go play with it on the ts-runtime playground.
Conclusion
TypeScript is not just a static analysis tool, in fact it is, but offers you the possibility of “making types reals”.
Type Guards is a beautiful and powerful feature of TypeScript that will allow your application to gain in maintainability and reliability.