DEV Community

Satel
Satel

Posted on

Using Predicate Combinators in Typescript

Problem:

Our predicate functions are checking too much and are in charge of more than should:

enum UserRole {
  Administrator = 1,
  Editor = 2,
  Subscriber = 3,
  Writer = 4,
}

interface User {
  username: string;
  age: number;
  role: UserRole;
}

const users = [
  { username: "John", age: 25, role: UserRole.Administrator },
  { username: "Jane", age: 7, role: UserRole.Subscriber },
  { username: "Liza", age: 18, role: UserRole.Writer },
  { username: "Jim", age: 16, role: UserRole.Editor },
  { username: "Bill", age: 32, role: UserRole.Editor },
];

const greaterThen17AndWriterOrEditor = users.filter((user: User) => {
  return (
    user.age > 17 &&
    (user.role === UserRole.Writer || user.role === UserRole.Editor)
  );
});

const greaterThen5AndSubscriberOrWriter = users.filter((user: User) => {
    return user.age > 5 && user.role === UserRole.Writer;
});
Enter fullscreen mode Exit fullscreen mode

Solution:

We have to start using the predicate combinators. It will increase the code readability and resuability.

type PredicateFn = (value: any, index?: number) => boolean;
type ProjectionFn = (value: any, index?: number) => any;

function or(...predicates: PredicateFn[]): PredicateFn {
  return (value) => predicates.some((predicate) => predicate(value));
}

function and(...predicates: PredicateFn[]): PredicateFn {
  return (value) => predicates.every((predicate) => predicate(value));
}

function not(...predicates: PredicateFn[]): PredicateFn {
  return (value) => predicates.every((predicate) => !predicate(value));
}
Enter fullscreen mode Exit fullscreen mode

You can use by combining those.

const isWriter = (user: User) => user.role === UserRole.Writer;
const isEditor = (user: User) => user.role === UserRole.Editor;
const isGreaterThan17 = (user: User) => user.age > 17;
const isGreaterThan5 = (user: User) => user.age > 5;

const greaterThan17AndWriterOrEditor = users.filter(
    and(isGreaterThan17, or(isWriter, isEditor))
);

const greaterThan5AndSubscriberOrWriter = users.filter(
    and(isGreaterThan5, isWriter)
);
Enter fullscreen mode Exit fullscreen mode

Better predicate combinators with factories

Predicate combinators create too many variables, so it’s easy to get lost between these functions. If we use the combinator predicate function only once, then it’s better to have something more generic.

const isWriter = (user: User) => user.role === UserRole.Writer;
const isEditor = (user: User) => user.role === UserRole.Editor;
const isGreaterThan17 = (user: User) => user.age > 17;
const isGreaterThan5 = (user: User) => user.age > 5;

const greaterThan17AndWriterOrEditor = users.filter(
    and(isGreaterThan17, or(isWriter, isEditor))
);

const greaterThan5AndSubscriberOrWriter = users.filter(
    and(isGreaterThan5, isWriter)
);
Enter fullscreen mode Exit fullscreen mode

We can solve this using the combinator predicate factories.

const isRole = (role: UserRole) => 
    (user: User) => user.role === role;

const isGreaterThan = (age: number) =>
    (user: User) => user.age > age;


const greaterThan17AndWriterOrEditor = users.filter(
    and(isGreaterThan(17), or(isRole(UserRole.Writer), isRole(UserRole.Editor)))
);

const greaterThan5AndSubscriberOrWriter = users.filter(
    and(isGreaterThan(5), isRole(UserRole.Writer))
Enter fullscreen mode Exit fullscreen mode

You’ve probably noticed that some function invocation is repeated with the same arguments. The best option will be to mix predicate factories with combinator predicates together. Thus we’ll have the best of both worlds.

const isRole = (role: UserRole) => 
    (user: User) => user.role === role;

const isGreaterThan = (age: number) =>
    (user: User) => user.age > age;

const isWriter = isRole(UserRole.Writer)

const greaterThan17AndWriterOrEditor = users.filter(
    and(isGreaterThan(17), or(isWriter, isRole(UserRole.Editor)))
);

const greaterThan5AndSubscriberOrWriter = users.filter(
    and(isGreaterThan(5), isWriter)
);
Enter fullscreen mode Exit fullscreen mode

In this way, we do fewer repeats and keep the code clean and neat.

Top comments (0)