DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Lucas Paganini
Lucas Paganini

Posted on • Originally published at lucaspaganini.com

Higher Order Guards (Functions)

TypeScript Narrowing #6


See this and many other articles at lucaspaganini.com

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.

function f1() {}
Enter fullscreen mode Exit fullscreen mode
function f1(f2: Function) {}
Enter fullscreen mode Exit fullscreen mode

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".

const makeIsNot = fn => ✨magic✨
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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

Higher 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.

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
}
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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.

Higher 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?

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

const isNotString = makeIsNot(isString);
Enter fullscreen mode Exit fullscreen mode

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.

makeIsNot 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.

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

const isNotString = makeIsNot(isString);
Enter fullscreen mode Exit fullscreen mode

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

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

const isNotString = makeIsNot(isString);
Enter fullscreen mode Exit fullscreen mode
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = (
  (fn) => (v) =>
    !fn(v)
)(isString);
Enter fullscreen mode Exit fullscreen mode

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.

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

const isNotNumber = makeIsNot(isNumber);
Enter fullscreen mode Exit fullscreen mode
const makeIsNot = (fn) => (v) => !fn(v);

const isNotNumber = (
  (fn) => (v) =>
    !fn(v)
)(isNumber);
Enter fullscreen mode Exit fullscreen mode

makeIsNot Signature

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

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);
Enter fullscreen mode Exit fullscreen mode

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

Type 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.

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);
Enter fullscreen mode Exit fullscreen mode

A little better, right?

Predicate 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.

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);
Enter fullscreen mode Exit fullscreen mode

Unpack 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.

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);
Enter fullscreen mode Exit fullscreen mode

Back 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.
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);
Enter fullscreen mode Exit fullscreen mode

Library

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.

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);
Enter fullscreen mode Exit fullscreen mode

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.

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

const isNotString = makeIsNot(isString);
Enter fullscreen mode Exit fullscreen mode
import { makeIsNot, makeIsInstance } from '@lucaspaganini/ts';

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

const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
const isFamousCat = makeIsIncluded(['Garfield', 'Tom']);
Enter fullscreen mode Exit fullscreen mode
  • 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.
npm install @lucaspaganini/ts
Enter fullscreen mode Exit fullscreen mode

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

Conclusion

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.

References

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

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Just kidding, it's a personal preference. But you can change your theme, font, etc. in your settings.

The more you know. 🌈