Using Predicate Combinators in Typescript
Tags: typescript
Published: 2022-06-24 17:15:51

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

```typescript
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;
});
```

## Solution:

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

```typescript
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));
}
```

You can use by combining those. 

```typescript
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)
);
```

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

```typescript
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)
);
```

We can solve this using the combinator predicate factories.

```typescript
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))
```

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.

```typescript
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)
);
```

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