Higher Order Guards (Functions) - TypeScript Narrowing #6

TypeScript Narrowing #6

Oofff, it’s part 6 already! I wonder how many of you are reading this since the beginning.

Today we'll grab a pattern from the functional programming world known as "Higher Order Functions" and use it to create functions that receive functions and return new functions.

TypeScript
function f1() {}
TypeScript
function f1(f2: Function) {}

But we won't stop there. We won't just return any new functions, more precisely, we will return new custom type guards! So, I'm calling those guard creation functions "Higher Order Guards".

TypeScript
const makeIsNot = fn => ✨magic✨

As you'll soon find out, that will open the door for new possibilities of reusing our code.

TypeScript
const makeIsNot = fn => ✨magic✨

const isNotString = makeIsNot(isString)

let aaa = 'abc' as  string | number | boolean
if (isNotString(aaa)) {
  aaa // <- aaa: number | boolean
} else {
  aaa // <- aaa: string
}

I'm Lucas Paganini, and in this blog, we release web development tutorials. Subscribe if you're interested in that.

LinkHigher Order Functions

The theoretical description of a higher order function is a tongue twister: a function that receives a function and returns another function. So let me show it to you in practice, and you'll see that it's not as complex as it sounds.

Let's say we have a lot of custom type guards, and now we want inverted versions of them.

  • We already have isString, now we want isNotString.
  • We already have isNumber, now we want isNotNumber.
  • You get the idea...

We did something very similar in the end of our third article, when we wrote a guard for truthy values that works by excluding falsy values.

TypeScript
type Truthy<T> = Exclude<T, Falsy>;

const isTruthy = <T extends unknown>(value: T): value is Truthy<T> =>
  value == true;

// Test
let x: null | string | 0;
if (isTruthy(x)) {
  x.trim(); // <- x: string
}

We can apply the same technique to create our inverted type guards. That's how they would look like:

TypeScript
const isNotString = <V extends unknown>(
  value: V
): value is Exclude<V, string> => isString(value) === false;

const isNotNumber = <V extends unknown>(
  value: V
): value is Exclude<V, number> => isNumber(value) === false;

But writing those inverted guards manually is tedious and repetitive. I bet you can see a pattern in them: all we need to create an inverted guard, is the custom type guard that will be inverted.

In other words: the only difference between isNotString and isNotNumber, is that while one uses the isString guard, the other uses the isNumber guard.

LinkHigher Order Guards

Could we stop repeating ourselves and create a function that accepts a type guard as an argument and returns the inverted version of the given type guard?

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);

Hell yeah we can! Let's create it now!

I have a personal convention of prefixing functions with the word make when they return new functions. So, it makes sense to me to call our function makeIsNot, since it makes the is not version of a type guard.

LinkmakeIsNot Implementation

The function implementation alone, is already a bit tricky, so I'll navigate it with you before we get into the TypeScript signature.

Let's use isNotString as an example.

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);

Calling makeIsNot with isString, returns a function that receives one argument (called v), and returns the inverted return of calling isString with v.

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);
TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = (
  (fn) => (v) =>
    !fn(v)
)(isString);

The same works for isNotNumber. Calling makeIsNot with isNumber, returns a function that receives one argument (called v), and returns the inverted return of calling isNumber with v.

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotNumber = makeIsNot(isNumber);
TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotNumber = (
  (fn) => (v) =>
    !fn(v)
)(isNumber);

LinkmakeIsNot Signature

All is good and well with the implementation, now, to the type signature of makeIsNot.

TypeScript
type MakeIsNot = <F extends (v: unknown) => v is any>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, F extends (v: unknown) => v is infer T ? T : never>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

Let's break it down and see what can be simplified.

LinkType Guard Function Type

First, there are two places where we're referring to a function that returns a type predicate (in other words, a custom type guard).

Let's create a type, called TypeGuardFunction, to isolate that type definition and simplify our code a little.

TypeScript
type TypeGuardFunction<T = any> = (v: unknown) => v is T;

type MakeIsNot = <F extends TypeGuardFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, F extends TypeGuardFunction<infer T> ? T : never>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

A little better, right?

LinkPredicate Function Type

Also, even though the naming TypeGuardFunction makes a lot of sense, since it is indeed a type guard function, this name is very specific to TypeScript. And it turns out that functions that receive an argument and return a boolean already had a name before TypeScript even existed. Those functions are known as "Predicate Functions".

So, let's use the name PredicateFunction instead.

TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, F extends PredicateFunction<infer T> ? T : never>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

LinkUnpack Predicate Function Type

Also, not that it repeats, but that part where we're inferring the type of the PredicateFunction is kinda ugly to look at. Let's isolate that in a type.

👉 If you're at a lost with the ternary operator and the infer keyword, I have two videos for you. Both are one minute long. One explains conditional types in TypeScript (the ternary operator), and the other explains the infer operator. Their links are in the description.

I have another personal convention which is to use the prefix Unpack when I'm creating a type that infers something. So I'll call it UnpackPredicateFunction.

TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, UnpackPredicateFunction<F>>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

LinkBack to the Signature

Ok... it's not simple. But it is simpler. Let's try to analyze the signature now.

  1. First, we receive an argument, a PredicateFunction called fn;
  2. Then, we return a new function;
  3. This new function receives an argument, called v. Which has the same type of the first parameter of fn;
  4. And that, returns a type predicate saying that v is not of the type guarded by our fn function.
TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, UnpackPredicateFunction<F>>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

LinkLibrary

There are some limitations to our function. For example, right now, it only works with guards that receive a single argument. Also, it's not tested.

TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, UnpackPredicateFunction<F>>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);

If you're interested in having makeIsNot in your codebase (and also makeIsInstance, makeIsIncluded, and a lot more), instead of copying the code from this article, a better way is to just install my TypeScript utilities library.

TypeScript
import { makeIsNot } from '@lucaspaganini/ts';

const isNotString = makeIsNot(isString);
TypeScript
import { makeIsNot, makeIsInstance } from '@lucaspaganini/ts';

const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
TypeScript
import { makeIsNot, makeIsInstance, makeIsIncluded } from '@lucaspaganini/ts';

const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
const isFamousCat = makeIsIncluded(['Garfield', 'Tom']);
  • It's open source
  • Has tests
  • Documentation
  • Works on Node and Browsers
  • It's MIT
  • And you can easily install it with npm install @lucaspaganini/ts.
bash
npm install @lucaspaganini/ts

We'll talk more about that library in the next article.

LinkConclusion

Today's content was pretty advanced. I remember how hard it was for me to learn functional programming and advanced TypeScript notations, so we really did our best with the examples and animations to hopefully, give you an easier learning experience than the one I had.

I would love to have a feedback from you. So, please send me a tweet and let us know if you could understand everything, and your questions, if you have any.

References are below. If you enjoyed the content, you know what to do.

And if your company is looking for remote web developers, consider contacting me and my team on lucaspaganini.com.

In the next article, we will use our newly found knowledge to create a workaround for a highly requested feature in TypeScript: asynchronous type guards. Subscribe if you don't want to miss it.

Until then, have a great day, and I’ll see you soon.

LinkReferences

  1. Higher Order Functions Clojure Documentation
  2. Functional Programming - Predicate Functions Stanford Education
  3. TypeScript Utilities Library - @lucaspaganini/ts GitHub Repository

Join our Newsletter and be the first to know when I launch a course, post a video or write an article.

This field is required
This field is required