5 TypeScript прийомів, щоб вивести ваш код на новий рівень 💪
З часом і практикою якість нашого коду росте. Однак завжди є місце для покращення. Щоразу, коли я дивлюся на код, який я написав півроку тому і не знаю, як його покращити я думаю про дві речі. Або я не виріс, або він уже у добрій формі. Якщо ви схожі на мене, і якість коду для вас дуже важлива, то нас це об'єднує. Швидше за все, ця стаття відкриє вам як мінімум кілька нових прийомів, про які ви можливо не знали і які, можливо, будете використовувати на регулярній основі.
Поговорим про:
- Покращена валідація за допомогою функції
includes
та селекторів - Використання колбеків для інкапсуляції коду
- Можливість використання комбінаторів предикатів (це тільки звучить страшно)
- Покращимо наші предикати за допомогою фабрик
- Інкапсулюємо алгоритм в окремий клас
Покращена валідація за допомогою функції includes
та селекторів
Проблема:
У нас є функція, яка порівнює властивість отриманого об'єкта з декількома значеннями одного типу і повертає true у випадку, якщо хоча б одне твердження позитивне.
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
);
}
Рішення:
Ми можемо використовувати метод масиву includes
. При такому підході нам не потрібна група перевірок через if і так код виглядає більш охайним
const EDIT_ROLES = [
UserStatus.Administrator,
UserStatus.Author,
UserStatus.Editor,
];
function isEditActionAvailable(user: User): boolean {
return EDIT_ROLES.includes(user.status);
}
Але тут є одна проблема.
По-перше, у нас є захардкоджені дані всередині функції або, інакше кажучи, у нас є неявний параметр EDIT_ROLES
.
По-друге, що якщо ми захочемо зробити ще одну guard-функцію, яка перевірятиме інші ролі?
Ми можемо зробити фабричну функцію, яка прийматиме функцію-селектор та дані, що відповідають за опис ролі користувача. Сама функція повертає іншу функцію, яка приймає користувача та порівнює його статус із раніше переданим списком ролей.
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);
Таким чином, ми відокремили дані від функції, що добре з функціональної точки зору, і зробили можливість її використання багаторазовим. Ось приклад того, наскільки легко ми можемо додати ще одну guard-функцію:
const ADD_ROLES = [
UserStatus.Administrator,
UserStatus.Author
];
const isAddActionAvailable = roleCheck((user: User) => user.status, ADD_ROLES);
Але зачекайте хвилинку. Ви, мабуть, думаєте про функцію селектора. Навіщо вона нам потрібна?
За допомогою функції selector можна вибирати різні поля.
Припустимо, користувач має властивість teamStatus
в якому зазначається його роль в команді, і нам потрібно перевірити чи є він керівником команди або ж менеджером. І тепер нам легко розробити guard-функцію, яка відповідає зазначеним вимогам.
// ...
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);
Використання колбеків для інкапсуляції коду
Проблема:
Ми маємо пару функцій, які схожі, але мають невеликі відмінності. Тому було б добре позбутися коду, що дублюється.
async function createUser(user: User): Promise<void> {
LoadingService.startLoading();
await userHttpClient.createUser(user);
LoadingService.stopLoading();
UserGrid.reloadData();
}
async function updateUser(user: User): Promise<void> {
LoadingService.startLoading();
await userHttpClient.updateUser(user);
LoadingService.stopLoading();
UserGrid.reloadData();
}
Рішення:
Ми можемо винести код який дублюється в окрему функцію, а "унікальну" частину коду передававати як колбек функцію.
async function makeUserAction(fn: Function): Promise<void> {
LoadingService.startLoading();
await fn();
LoadingService.stopLoading();
UserGrid.reloadData();
}
async function createUser2(user: User): Promise<void> {
makeUserAction(() => userHttpClient.createUser(user));
}
async function updateUser2(user: User): Promise<void> {
makeUserAction(() => userHttpClient.updateUser(user));
}
Використання комбінаторів предикатів (це тільки звучить страшно)
Предикатні функції – це функції, які здійснюють перевірку певної умови (предиката) та повертають значення true
або false
Проблема:
Наші предикатні функції перевіряють занадто багато умов
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;
});
Рішення:
Ми маємо почати використовувати комбінатори предикатів. Це підвищить читабельність коду та можливість повторного використання.
type PredicateFn = (value: any, index?: number) => boolean;
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));
}
Розглянемо комбінаторні предикати у дії:
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)
);
Покращуємо наші предикати за допомогою фабрик
Проблема:
Комбінатори предикатів створюють занадто багато змінних, тому легко заблукати між цими функціями. Якщо ми використовуємо комбінаторну функцію предикату лише один раз, то краще мати щось спільне.
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)
);
Рішення:
Ми повинні використовувати комбінаторні фабрики предикатів. Давайте додамо кілька:
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))
);
Ви, ймовірно, помітили, що деякі виклики функцій повторюються з тими самими аргументами. Найкращим варіантом буде змішання фабрик предикатів із комбінаторними предикатами. Таким чином, ми отримаємо найкраще з обох сторін.
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)
);
В кінцевому результаті ми робимо менше повторень і зберігаємо код чистим та акуратним.
Інкапсулюємо алгоритм в окремий клас
Проблема:
У нас є клас, який відповідає за дуже багато речей. І одним з них для прикладу є відформатований вивід його данних. Справедливо сказати, що цей метод не повинен перебувати в моделі даних користувача. Оскільки з часом логіка цієї функції може збільшитись в рази.
class User {
constructor(
public firstName: string,
public lastName: string,
public signUpDate: Date
) {}
getFormattedUserDetails(): string {
const formattedSignUpDate = `${this.signUpDate.getFullYear()}-${this.signUpDate.getMonth() + 1}-${this.signUpDate.getDate()}`;
const username = `${this.firstName.charAt(0)}${this.lastName}`.toLowerCase();
return `
First name: ${this.firstName},
Last name: ${this.lastName},
Sign up date: ${formattedSignUpDate},
Username: ${username}
`;
}
}
const user = new User("John", "Doe", new Date());
console.log(user.getFormattedUserDetails());
Рішення:
Тому наше завдання – розділити відповідальність. Для цього ми можемо даний алгоритм інкапсулювати в новий клас, який буде виконувати тільки цю одну задачу.
interface User {
firstName: string,
lastName: string,
signUpDate: Date
}
class UserDetailsFormatter {
constructor(private user: User) {}
format(): string {
const { firstName, lastName } = this.user;
return `
First name: ${firstName},
Last name: ${lastName},
Sign up date: ${this.getFormattedSignUpDate()},
Username: ${this.getUsername()}
`;
}
private getUsername(): string {
const { firstName, lastName } = this.user;
return `${firstName.charAt(0)}${lastName}`.toLowerCase();
}
private getFormattedSignUpDate(): string {
const signUpDate = this.user.signUpDate;
return [
signUpDate.getFullYear(),
signUpDate.getMonth() + 1,
signUpDate.getDate(),
].join("-");
}
}
const user = { firstName: "John", lastName: "Doe", signUpDate: new Date() };
const userFormatter = new UserDetailsFormatter(user);
console.log(userFormatter.format());
Висновок
Я сподіваюся, що ця стаття була для вас цікавою і ви дізналися щось нове і корисне. Якщо у вас залишились питання - пишіть в коментарях 🔽