Сьогодні важко уявити надійний JavaScript-додаток без використання TypeScript. Такі функції, як інтерфейси, кортежі та узагальнення, добре знайомі розробникам TypeScript. Хоча вивчення деяких просунутих концепцій може потребувати додаткових зусиль, вони можуть значно підвищити безпеку типів. У цій статті ви дізнаєтесь про деякі з них.
Захисники типів
Захисники типів дозволяють визначити тип значення в умовному блоці. Існує декілька простих способів виконання перевірки типу за допомогою операторів in, typeof та instanceof, або за допомогою порівняння на рівність (===).
У цьому розділі ми зосередимося на користувацьких засобах захисту типів. Це прості функції, які повертають булеве значення, діючи як предикат типу. Розглянемо приклад, де ми працюємо з основною інформацією про користувача та користувачами з додатковими даними.
type User = { name: string };
type DetailedUser = {
name: string;
profile: {
birthday: string
}
}
function isDetailedUser(user: User | DetailedUser) {
return ‘profile’ in user;
}
function showDetails(user: User | DetailedUser) {
if (isDetailedUser(user)) {
console.log(user.profile); // Error: Property ‘profile’ does not exist on type ‘User | DetailedUser’.
}
}
Функція isDetailedUser повертає логічне значення, але не вказує, що ця функція визначає тип об'єкта.
Щоб досягти бажаного результату, функцію isDetailedUser потрібно оновити за допомогою конструкції user is DetailedUser.
function isDetailedUser(user: User | DetailedUser): user is DetailedUser {
return ‘profile’ in user;
}
Індексовані типи доступу
У деяких випадках ваша програма може використовувати великий тип об'єкта, але для створення нового типу вам потрібна лише його частина. Наприклад, якщо певній частині програми потрібен лише профіль користувача, ви можете використати User['profile'] для вилучення потрібного типу і присвоїти його новому типу UserProfile.
type User = {
id: string;
name: string;
surname: string;
profile: {
birthday: string;
}
}
type UserProfile = User[‘profile’];
Якщо вам потрібно створити тип на основі вибраних властивостей, ви можете скористатися вбудованим типом Pick.
type FullName = Pick<User, ‘name’ | ‘surname’>; // { name: string; surname: string }
Існує кілька інших типів утиліт, таких як Omit, Exclude і Extract, які можуть бути корисними для вашої програми. Хоча на перший погляд вони можуть здатися індексованими типами, насправді вони побудовані на основі відображених типів.
Індексовані типи з масивом
Можливо, вам доводилося стикатися з ситуацією, коли програма надавала об'єднаний тип, такий як:
type UserRoleType = ‘admin’ | ‘user’ | ‘newcomer’;
В іншій частині програми отримуються дані користувачів і перевіряється їхня роль. Для цього сценарію необхідно створити масив:
const ROLES: UserRoleType[] = [‘admin’, ‘user’, ‘newcomer’];
ROLES.includes(response.user_role);
Здається виснажливим, чи не так? Повторення значень типу union у масиві може бути нудним. Було б ідеально витягувати тип безпосередньо з існуючого масиву, щоб уникнути надмірності. На щастя, індексовані типи надають рішення для цього.
Для початку нам потрібно визначити наш масив за допомогою константного твердження. Це допомагає усунути дублювання і створює кортеж, доступний лише для читання.
const ROLES = [‘admin’, ‘user’, ‘newcomer’] as const;
Потім, використовуючи оператор typeof разом з числовим типом, ми можемо створити об'єднаний тип, похідний від значень у масиві.
type RolesType = typeof ROLES[number]; // ‘admin’ | ‘‘user’ | ‘‘newcomer’;
Такий підхід може здатися незрозумілим на перший погляд, але важливо пам'ятати, що масиви - це об'єктні структури з числовими ключами. З цієї причини у цьому прикладі як тип доступу до індексу використовується тип числа.
Умовні типи та ключове слово infer
Умовні типи визначають тип, який змінюється залежно від певної умови. Вони часто використовуються у парі з узагальненими типами для визначення вихідного типу на основі вхідного типу. Наприклад, вбудований тип NonNullable у TypeScript реалізовано за допомогою умовних типів.
type NonNullable<T> = T extends null | undefined ? never : T
type One = NonNullable<number>; // number
type Two = NonNullable<undefined>; // never
Ключове слово infer використовується спеціально з умовними типами і обмежується оператором extends. Його основне призначення - діяти як «type variable creator». Розуміння його функціональності стає зрозумілішим, якщо розглянути його на практичному прикладі.
Приклад: отримати тип результату асинхронної функції.
const fetchUser = (): Promise<{ name: string }> => { /* implementation */ }
Найпростіший підхід - імпортувати оголошення типу і присвоїти його змінній. Однак бувають ситуації, коли оголошення результату записується безпосередньо всередині функції, як показано у прикладі вище.
Цю проблему можна вирішити у два кроки:
1. Тип утиліти Awaited, введений в TypeScript 4.5, пропонує рішення. У навчальних цілях розглянемо його спрощену версію.
export type Awaited<T> = T extends Promise<infer U> ? U : T;
Комбінуючи умовні типи та ключове слово infer, обіцяний тип витягується та присвоюється Uname, фактично оголошуючи змінну типу. Якщо наданий тип сумісний з узагальненим типом PromiseLike, конструкція повертає оригінальний тип, що зберігається в Uname.
2. Отримання значення з асинхронної функції
Використовуючи вбудований ReturnType, який отримує тип повернення функції, разом з користувацьким типом Awaited, ми можемо досягти бажаного результату.
export type AwaitedReturnType<T> = Awaited<ReturnType<T>>;
Сподіваємось, ця стаття була для вас корисною. Насолоджуйтесь кодуванням!