Scroll to top

DevKucher;

5 TypeScript прийомів, щоб вивести ваш код на новий рівень 💪

Изображение на обложке для 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());

Висновок

Я сподіваюся, що ця стаття була для вас цікавою і ви дізналися щось нове і корисне. Якщо у вас залишились питання - пишіть в коментарях 🔽