TypeScript: User Defined Type Guards

This is the English version of this article.

この記事の日本語版はこちらです。

I recently discovered a TypeScript language feature called “user-defined type guards”, which let you communicate type information across function boundaries.

Let’s start from the problem definition.

Type Narrowing

In TypeScript, you often need to check data for existence before using it.

function plusOne(value: number) {
  console.log(value + 1)
}

let x?: number

// Error! `x` may not be a number.
plusOne(x)

if (x !== undefined) {

  // `x` is definitely a number.
  plusOne(x)
}

By checking for undefined, we narrow the potential types of x and ensure null-safety for the code.

In practice, the type of x might be an instance member. You may be forced to write the existence check multiple times as a result.

class Example {

  value?: number

  plusOne() {
    if (this.value !== undefined) {
      console.log(this.value + 1)
    }
  }

  plusTwo() {
    if (this.value !== undefined) {
      console.log(this.value + 2)
    }
  }
}

At this point, we want to refactor the check for more DRY code.

class Example {

  value?: number

  checkValue(value?: number): boolean {
    return value !== undefined
  }
}

Unfortunately, the check occurs in a different function scope, so TypeScript doesn’t narrow the type.

class Example {

  ...

  process() {
    if (this.checkValue(this.value)) {

      // Error! `value` may be `undefined`.
      this.plusOne(this.value)
    }
  }
}

User-Defined Type Guards

There’s an advanced TypeScript language feature that solves this problem. It’s called user-defined type guards.

As the name indicates, the feature lets you annotate functions with type-narrowing information. To solve the example above, you would write:

class Example {

  value?: number

  checkValue(value?: number): value is number {
    return value !== undefined
  }
}

The new syntax indicates that if the return value is true, the value is guaranteed to no longer be undefined. Now we can share the function.

Conclusion

To be honest, TypeScript should automatically infer the flowed type across non-asynchronous function boundaries, ideally based off a setting in tsconfig.js in case of TypeScript environments that support interruptions in synchronous code.

But until then, it’s another tool for writing more type-safe code.