type_guards_and_closures

Posted in Hybrid, React, typescript on September 8, 2023 by Duco ‐ 6 min read

Type guards & closures

This blogpost is written by our Hybrid developer Duco

Are you struggling with TypeScript’s union types? If you’re tired of losing autocomplete and using type assertions just to avoid error messages, read on! In this article, we’ll explore a better way to use find and filter functions while narrowing union types with type guards and closures. We’ll also delve into generics to create more flexible type guards that can be used across various types.

Take for example the following types:

type A = {
  type: 'A';
  foo: any;
};

type B = {
  type: 'B';
  bar: any;
};

type Union = A | B;

and check out the following code:

let unionArray: Union[];

const foundUnion = unionArray.find((union) => union.type === 'A');
// we would expect foundUnion to be of type `A | undefined`
// because it is either found or not, but observe that it is of type `Union | undefined`
// so code below throws a TS error, because TS doesn't know what type the foundUnion is
console.log(foundUnion?.foo);

This can be very frustrating because we as developers know for sure that the type should be A | undefined because of the find (or filter, does the same thing), but TypeScript doesn’t know. So we lose autocomplete. Sometimes we resort to using type assertions to circumvent these errors:

const foundUnion = unionArray.find((union) => union.type === 'A') as A | undefined

But there is a better (and type-safe) way!

Introducing the type guard

The issue above can be partially solved by using a type guard function. A type guard function is a special function that explicitly tells TypeScript whether something is of a certain type. It’s return type is a type predicate:

// this is the type guard function: its return type is typed as `union is A`
// where `union` is the input param, and `A` the type we explicitly tell TypeScript it is
// when the type guard returns true
const unionIsTypeA = (union?: Union): union is A => {
  return union?.type === 'A';
};
const unionArray: Union[] = []; // <-- now empty, but can contain data

const foundUnion = unionArray.find((union) => union.type === 'A'));
// still throws TS error, but
console.log(foundUnion?.foo);

if (unionIsTypeA(foundUnion)) {
  // this doesn't! we have successfully told TS that `foundUnion` is of type `A` 
  console.log(foundUnion.foo);
}

But why this extra if check after the find? We should be able to use the type guard function in the find directly, and TS would know foundUnion is of type A , right?

const foundUnion = unionArray.find((union) => unionIsTypeA(union));
// still throws a TS error!!
// foundUnion is still of type `Union` instead of `A`
console.log(foundUnion?.foo);

As we can observe, when using the type guard function directly, TypeScript does not narrow down the type even when using the type guard. The problem is that a type guard function is just a function that returns a boolean. We told TypeScript that the meaning of that boolean is the type narrowing, but it loses that meaning when just used in a function (for example find or filter).

This is easily fixed by directly using the type guard:

const foundUnion = unionArray.find(unionIsTypeA); // <-- just pass the type guard directly as input
// No TS error anymore, and we have autocomplete!
console.log(foundUnion?.foo);

Now I hear you think, but what if we have more than just A and B, would we need to create type guards for every type? This needs to be more generic!

Introducing generics

So let’s rewrite our type guard function to accept a param with the type we want it to have checked.

The extra param type will be either 'A' or 'B' in this case. To be more specific, the type param should be of the same type as the type property of our Union. In code, this would be Union['type']

The meaning of the returned boolean of our type guard changes based on it’s type input. If we pass type: 'A' our type guard checks if our input Union is of type A, but if we pass 'B', it checks if our input is of type B. We need to reflect this in our type predicate.

Here we can use the TS built-in type Extract. Extract takes a Type generic and a Union generic, and constructs a type by extracting from Type all union members that are assignable to Union. In our case this would be Extract<Union, { type: Union['type'] }> e.g. Extract<Union, { type: 'A' }> would result in type A

To combine the two things above in our type guard, we use a generic T that extends Union['type'], so the type predicate will automatically change based on the type input we give it:

const unionIsType = <T extends Union['type']>(union: Union | undefined, type: T): union is Extract<Union, { type: T }> => {
  return union?.type === type;
};

Usage of our more generic type guard

// union is a variable containing either an object of type A or type B
if (unionIsType(union, 'A')) {
  // yay! no error!
  console.log(union.foo);
}

const foundUnion = unionArray.find((union) => unionIsType(union, 'A'));
// What? This error again?
console.log(foundUnion?.foo);

Because our type guard accepts a second param type now, we cannot pass it to find directly anymore as we did earlier. Now we are back at square 1, where TypeScript again doesn’t narrow it’s types down.

Introducing closures

A closure is a function that encapsulates some data in it’s scope. In practice this is a function that returns a function. Here we can make use of a closure to build a function that can be directly passed as our find predicate function.

export const unionIsTypeClosure = (type) => {
  return (union: Union) => unionIsType(union, type);
};

We can run the closure first, and pass its output directly to find:

const predicateFunction = unionIsTypeClosure('A');
const foundUnion = unionArray.find(predicateFunction);

or directly

const foundUnion = unionArray.find(unionIsTypeClosure('A'));

Now note that this again doesn’t narrow down the types, as we have not specified that the return type of our closure is a type guard. Remember: a type guard is just a function that returns a boolean, without the type predicate it has no meaning (for TypeScript anyways).

So let’s modify our closure to make it more type-safe, and explicitly return a type guard function.

export const unionIsTypeClosure = <T extends Union['type']>(type: T) => {
  return (union: Union): union is Extract<Union, { type: T }> => unionIsType(union, type);
};

Putting it all together

Let’s revisit our original code we started with

let unionArray: Union[];

const foundUnion = unionArray.find((union) => union.type === 'A');
// we would expect foundUnion to be of type `A | undefined`
// because it is either found or not, but observe that it is of type `Union | undefined`
// so code below throws a TS error, because TS doesn't know what type the foundUnion is
console.log(foundUnion?.foo);

With our new closure we can rewrite it to

let unionArray: Union[];

const foundUnion = unionArray.find(unionIsTypeClosure('A'));
// our error below has vanished 🎉
console.log(foundUnion?.foo);

Read more

Want to ready more about this subject. Take a look at the following links 👇 https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures