Advanced Typescript: Narrowing & Predicates

In the last part of this series, we talked about how you can infer types in Typescript. In this part, we’ll take a look at a related use-case: making the Typescript compiler happy by narrowing the type of a sum type.

What’s Type Narrowing?

Let’s first describe the problem. In Todoist, you can complete & archive both tasks and sections. Let’s say we model it like this:

type TaskCompletedData = {
    subTasks: Task[]
    showHint: boolean
    cursor?: string

type SectionCompletedData = {
    completedItems: CompletedTask[]
    tasksLeft: number
    showHint: boolean
    cursor?: string

Now it doesn’t really matter what these properties are, I’m just going to use them for illustration, but if we want to build a function to work with a generic completedData variable, it can become hard to “narrow down the type” to what we’re interested in. See for example:

function getCompletedCount(completedData: TaskCompletedData | SectionCompletedData) {
    //how do I make Typescript happy here?

The answer is to use the Javascript in operator to check for existence of properties:

function getCompletedCount(completedData: TaskCompletedData | SectionCompletedData) {
    if ('completedItems' in completedData) {
        return getSectionsCount(completedData)
    } else if ('subTasks' in completedData) {
        return getItemsCount(completedData)
    } else {
        throw `Unexpected value for completedData: ${completedData}`

Why can’t you simply call getSectionsCount direct? That’s because Typescript can’t really guess the type of a variable at compile-time (& there’s no Typescript at runtime). Writing the function in this way means you have adequate guards in place so that when Typescript does its job of compiling down, you still have reasonably safe code when all types are erased. It’s not the prettiest of code for sure, but it’s part of the tradeoff of using a fully-erased type system.

Type Predicates

A counterpart to type narrowing is type assertions and type predicates. Note type assertions are the most commonly abused system to “force” the compiler to understand one type as another. The syntax is pretty simple:

const a = "Hello"
getCount(a as any)

In the above example, we are widening the type of a so that it’s compatible with a possible different type that the getCount function expects. Using as casting in this way is frowned upon and possibly never has a place in a good codebase. Type predicates however, are a safer way to achieve the same thing.

Note: to use type predicates, you need a reasonably newer version of Typescript. But it doess make writing these type guards a bit cleaner. Here’s an assumeNotNull Jest test helper that we have in both the Todoist and Twist codebases.

function assumeNotNull<T>(value: T): asserts value is NonNullable<T> {

The current Jest type definitions for expect(value).not.toBeNull() don’t really communicate with Typescript that subsequent calls to expect(value) will always have value as not null (this is because the first expectation will throw and break the test). Here we’re writing a helper function to make this explicit. The important bit to note here is the asserts value is bit, which is called a type predicate. These type predicates are very flexible, and can be used to nudge Typescript in a lot of different ways.

Now both predicates and type narrowing may seem like extra boilerplate that comes with Typescript adoption, and if you are used to writing Javascript, initialy it definitely does seem like that. I sometimes do think of these as “stuff I do to keep the Typescript compiler happy.” From a longer-term software maintanence perspective though, building a strict tsconfig, and sticking to a well-typed codebase has huge rewards. There’s nothing wrong in prototyping your solution in a .JS file, without even having any types at all. And once you start converting the solution to Typescript, the compiler warnings and errors will guide you to writing more safe code.

Well that’s it for this instalment. See you next time!

Recommending fnm for node version switching

So I’m a distant admirer of Reason, and one of the benefits it brings is the ability to have a single language that both compiles down to Javascript so you can use it on the web, and also have a native backend so you can write incredibly fast command-line tools.

fnm is one such native ReasonML tool that’s at least 100x faster than any other node version switcher. Recommended!

The Squiggle of the Design Process

From the parent post, which describes this far more elegantly, but this made me think of how programming a product is sometimes very similar. You are told to build a feature, and you start by exploring how to go about it, there’s a bunch of googling, a whole lot of visits to Stack Overflow, a lot of exploration around the codebase, until you have a concept.

The only difference is, from there it’s hardly a straight line. Similar yes, but oh so different.

— Read on