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!

Advanced Typescript: Inference

So in part 1 of this series, we looked at how the typeof operator can be used to infer the Typescript type of an object. There’s a bunch more ways to infer types, and it makes working with Typescript pretty interesting as you can make the type inference do the work for you. Here’s a few of them:

Infer the ReturnType of a function

Works like you’d expect:

> function isEmpty(s: string | null | undefined) { return s === "" || s === null || s === undefined }

> let a: ReturnType<typeof isEmpty>

> a = "hello"
[eval].ts:3:1 - error TS2322: Type '"hello"' is not assignable to type 'boolean'.

3 a = "hello"


Note the slightly weird construction: ReturnType <typeof isEmpty>. That’s because ReturnType (just like all Typescript types) expects a type as a parameter, and not a value.

Infer the type of an instance from the Class Name

Just like ReturnType, there’s also InstanceType:

> class Trigger { x = 0; y = 0 }

> let g: InstanceType<typeof Trigger> = "hello"
[eval].ts:14:5 - error TS2322: Type '"hello"' is not assignable to type 'Trigger'.

Infer the type of Props of a React Component

Here’s a code fragment from Reactist test suite that illustrates this:

    const getAvatar = (
        props?: Omit<React.ComponentProps<typeof Avatar>, 'user'> & {
            user?: { name?: string; email: string }
    ) => (
            user={{ name: 'Henning Mus', email: '' }}

As you can see React.ComponentProps works pretty much like ReturnType, so you don’t really need to export Props from a component ever. (h/t Janusz)

Typescript has far more complex inference (the infer keyword comes to mind), but this is good enough to start and really simplifies a lot of use-cases, and avoids unneeded type exports from a library.

Advanced Typescript

Typescript is a very nice and pragmatic addition to Javascript, and enables a bunch of nice programming safety features that would be more difficult to implement without it. I’ve been reading the book Effective Typescript recently, and I thought I’ll start a short series on the tips and tricks I’ve picked up from it. So here’s part 1 of a 5 part series on some advanced things you can do with Typescript.

To Follow Along

Please install ts-node and typescript globally:

npm install ts-node typescript -g

& then just ts-node should give you a terminal:

❯ ts-node
> 1 + 1
> const a = "hello"
> a

You can use this to get a Typescript REPL & then follow-along with the rest of the examples.

typeof operator

typeof is a Javascript construct that gives you a type of an object. If you use this in Javascript, then you get simple Javascript types:

> const a = {hello: "world"}
> typeof a

But in a type context, the same operator can be used for much richer type information:

> let b: typeof a;
> b = {hello: 1}
[eval].ts:4:6 - error TS2322: Type 'number' is not assignable to type 'string'.
4 b = {hello: 1}
    1 const a = {hello: "world"}
    The expected type comes from property 'hello' which is declared here on type '{ hello: string; }'

As you can see, using typeof as a type annotation on the left side of the new variable b produced the inferred type { hello: string }, a much more complex type than just object and Typescript good at this kind of inference. It works for more than simple variables too:

> let isNegative = (b: number) => b < 0
> let isPositive: typeof isNegative = "hello"
[eval].ts:7:5 - error TS2322: Type '"hello"' is not assignable to type '(b: number) => boolean'.
7 let isPositive: typeof isNegative = "hello"

Here we see that we can use typeof to infer the type of a function. This can be used in several different places, and even when you import values or functions from an external library that has types.

Constructing Partial Types with Pick and Omit

You can use Pick and Omit to construct subset of types pretty easily:

> let metadata = {
... isOpen: false,
... isDraft: false,
... isComplete: true
... }
> type Metadata = typeof metadata;
> type SaveMetadata = Pick<Metadata, "isOpen" | "isComplete">

Here we’re defining a new type SaveMetadata as being derived from Metadata but having picked up only the isOpen and isComplete keys. This saves duplication & makes the code a lot more DRY. Omit is the reverse:

> type SaveMetadata = Omit<Metadata, "isDraft">

These are the same types written in two different ways.

That’s it for today! More Typescript goodness when we meet next!

JS Rockstars: Make everybody play well together!

I’ve built a small Typescript project that makes a bunch of Javascript libraries play well together. It looks like this:

I used to be a 100% Ruby guy, but I’ve learnt a lot about JavaScript and the Typescript world in the last year. A lot of the experience is great, and frankly, far better than you’d find in the Ruby world, but one thing that is clearly inferior is how many choices there are, and how all of them are “much too big to play well together”[1]. This is an attempt to fix that, an opinionated project that uses the best JS libraries out there to build out a full stack application.

You can see more info about it here: js-rockstars. Do give it a whirl and let me know feedback! 🙂

[1]: Much like one of my favourite NBA teams from the early 2000s.