Problem:
We have a function that compares the same object property with multiple values of the same type and returns true in case if at least one statement is positive.
enum UserStatus {
Administrator = 1,
Author = 2,
Contributor = 3,
Editor = 4,
Subscriber = 5,
}
interface User {
firstName: string;
lastName: string;
status: UserStatus;
}
function isEditActionAvailable(user: User): boolean {
return (
user.status === UserStatus.Administrator ||
user.status === UserStatus.Author ||
user.status === UserStatus.Editor
);
}
Solution:
We can use an array method includes. With this approach, an if statement will look cleaner than before.
const EDIT_ROLES = [
UserStatus.Administrator,
UserStatus.Author,
UserStatus.Editor,
];
function isEditActionAvailable(user: User): boolean {
return EDIT_ROLES.includes(user.status);
}
But there is a catch.
First, we have hard-coded data inside the function, or to put it in another way we have an implicit input (EDIT_ROLES).
Second, what if we want to make another role guard function that will be checking different action roles?
We can provide a factory function that will take a selector function and data responsible for describing a user role. The function itself returns another function that takes a user and compares his status with the previously passed role list.
function roleCheck<D, T>(selector: (data: D) => T, roles: T[]): (value: D) => boolean {
return (value: D) => roles.includes(selector(value));
}
const isEditActionAvailable = roleCheck((user: User) => user.status, EDIT_ROLES);
In this way, we've split the data from the function, what is good from the functional standpoint, and made it reusable.
Here is an example of how easy we can add another role guard function:
const ADD_ROLES = [
UserStatus.Administrator,
UserStatus.Author
];
const isAddActionAvailable = roleCheck((user: User) => user.status, ADD_ROLES);
But wait a minute. You probably think about the selector function. Why do we need it?
With the help of the selector function, it's possible to select different fields.
Suppose a user has a team status role field, and we have to check whether he is a team lead or manager. It's effortless to develop a guard function that follows mentioned requirements by using an already implemented function.
// ...
enum TeamStatus {
Lead = 1,
Manager = 2,
Developer = 3
}
interface User {
firstName: string;
lastName: string;
status: UserStatus;
teamStatus: TeamStatus;
}
function roleCheck<D, T>(selector: (data: D) => T, roles: T[]): (value: D) => boolean {
return (value: D) => roles.includes(selector(value));
}
const MANAGER_OR_LEAD = [
TeamStatus.Lead,
TeamStatus.Manager
]
const isManagerOrLead = roleCheck((user: User) => user.teamStatus, MANAGER_OR_LEAD);