Дослідіть брендовані типи TypeScript — потужну техніку для досягнення номінальної типізації у структурній системі типів. Дізнайтеся, як покращити безпеку типів та ясність коду.
Брендовані типи в TypeScript: Номінальна типізація у структурній системі
Структурна система типів TypeScript пропонує гнучкість, але іноді може призводити до неочікуваної поведінки. Брендовані типи надають спосіб застосувати номінальну типізацію, підвищуючи безпеку типів та ясність коду. Ця стаття детально розглядає брендовані типи, надаючи практичні приклади та найкращі практики для їх реалізації.
Розуміння структурної та номінальної типізації
Перш ніж заглибитися у брендовані типи, давайте роз'яснимо різницю між структурною та номінальною типізацією.
Структурна типізація ("качина" типізація)
У структурній системі типів два типи вважаються сумісними, якщо вони мають однакову структуру (тобто однакові властивості з однаковими типами). TypeScript використовує структурну типізацію. Розглянемо цей приклад:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Допустимо в TypeScript
console.log(vector.x); // Вивід: 10
Хоча Point
та Vector
оголошені як різні типи, TypeScript дозволяє присвоювати об'єкт Point
змінній типу Vector
, оскільки вони мають однакову структуру. Це може бути зручно, але також може призвести до помилок, якщо потрібно розрізняти логічно різні типи, які випадково мають однакову форму. Наприклад, координати широти/довготи, які можуть випадково збігатися з координатами пікселів на екрані.
Номінальна типізація
У номінальній системі типів типи вважаються сумісними, лише якщо вони мають однакову назву. Навіть якщо два типи мають однакову структуру, вони розглядаються як різні, якщо мають різні назви. Мови, такі як Java та C#, використовують номінальну типізацію.
Потреба у брендованих типах
Структурна типізація TypeScript може бути проблематичною, коли вам потрібно гарантувати, що значення належить до певного типу, незалежно від його структури. Наприклад, розглянемо представлення валют. Ви можете мати різні типи для USD та EUR, але обидва можуть бути представлені як числа. Без механізму для їх розрізнення ви можете випадково виконати операції з неправильною валютою.
Брендовані типи вирішують цю проблему, дозволяючи створювати окремі типи, які є структурно схожими, але розглядаються системою типів як різні. Це підвищує безпеку типів і запобігає помилкам, які інакше могли б прослизнути.
Реалізація брендованих типів у TypeScript
Брендовані типи реалізуються за допомогою типів-перетинів та унікального символу або рядкового літерала. Ідея полягає в тому, щоб додати "бренд" до типу, який відрізняє його від інших типів з такою ж структурою.
Використання символів (рекомендовано)
Використання символів для брендування є кращим, оскільки символи гарантовано є унікальними.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Розкоментування наступного рядка спричинить помилку типу
// const invalidOperation = addUSD(usd1, eur1);
У цьому прикладі USD
та EUR
— це брендовані типи, засновані на типі number
. unique symbol
гарантує, що ці типи є різними. Функції createUSD
та createEUR
використовуються для створення значень цих типів, а функція addUSD
приймає лише значення USD
. Спроба додати значення EUR
до значення USD
призведе до помилки типу.
Використання рядкових літералів
Ви також можете використовувати рядкові літерали для брендування, хоча цей підхід менш надійний, ніж використання символів, оскільки рядкові літерали не гарантовано є унікальними.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Розкоментування наступного рядка спричинить помилку типу
// const invalidOperation = addUSD(usd1, eur1);
Цей приклад досягає того ж результату, що й попередній, але з використанням рядкових літералів замість символів. Хоча цей спосіб простіший, важливо переконатися, що рядкові літерали, які використовуються для брендування, є унікальними у вашій кодовій базі.
Практичні приклади та випадки використання
Брендовані типи можна застосовувати в різних сценаріях, де потрібно забезпечити безпеку типів поза межами структурної сумісності.
Ідентифікатори (ID)
Розглянемо систему з різними типами ідентифікаторів, такими як UserID
, ProductID
та OrderID
. Усі ці ID можуть бути представлені числами або рядками, але ви хочете запобігти випадковому змішуванню різних типів ID.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... отримати дані користувача
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... отримати дані продукту
return { name: "Example Product", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("User:", user);
console.log("Product:", product);
// Розкоментування наступного рядка спричинить помилку типу
// const invalidCall = getUser(productID);
Цей приклад демонструє, як брендовані типи можуть запобігти передачі ProductID
у функцію, яка очікує UserID
, підвищуючи безпеку типів.
Значення, специфічні для домену
Брендовані типи також можуть бути корисними для представлення специфічних для домену значень з обмеженнями. Наприклад, ви можете мати тип для відсотків, які завжди повинні бути в межах від 0 до 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('Відсоток має бути від 0 до 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Discounted Price:", discountedPrice);
// Розкоментування наступного рядка спричинить помилку під час виконання
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Цей приклад показує, як забезпечити виконання обмеження для значення брендованого типу під час виконання. Хоча система типів не може гарантувати, що значення Percentage
завжди буде між 0 і 100, функція createPercentage
може забезпечити це обмеження під час виконання. Ви також можете використовувати бібліотеки, як-от io-ts, для валідації брендованих типів під час виконання.
Представлення дати та часу
Робота з датами та часом може бути складною через різноманітність форматів та часових поясів. Брендовані типи можуть допомогти розрізнити різні представлення дати та часу.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Перевірити, що рядок дати у форматі UTC (наприклад, ISO 8601 з Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Неправильний формат дати UTC');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Перевірити, що рядок дати у форматі локальної дати (наприклад, YYYY-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Неправильний формат локальної дати');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Виконати перетворення часового поясу
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("UTC Date:", utcDate);
console.log("Local Date:", localDate);
} catch (error) {
console.error(error);
}
Цей приклад розрізняє дати UTC та локальні дати, гарантуючи, що ви працюєте з правильним представленням дати та часу в різних частинах вашої програми. Валідація під час виконання гарантує, що лише правильно відформатовані рядки дати можуть бути присвоєні цим типам.
Найкращі практики використання брендованих типів
Щоб ефективно використовувати брендовані типи в TypeScript, дотримуйтесь таких найкращих практик:
- Використовуйте символи для брендування: Символи надають найсильнішу гарантію унікальності, зменшуючи ризик помилок типів.
- Створюйте допоміжні функції: Використовуйте допоміжні функції для створення значень брендованих типів. Це забезпечує централізовану точку для валідації та гарантує послідовність.
- Застосовуйте валідацію під час виконання: Хоча брендовані типи підвищують безпеку типів, вони не запобігають присвоєнню неправильних значень під час виконання. Використовуйте валідацію під час виконання для забезпечення обмежень.
- Документуйте брендовані типи: Чітко документуйте призначення та обмеження кожного брендованого типу для покращення супроводу коду.
- Враховуйте вплив на продуктивність: Брендовані типи створюють невеликі накладні витрати через тип-перетин та потребу в допоміжних функціях. Враховуйте вплив на продуктивність у критично важливих частинах вашого коду.
Переваги брендованих типів
- Покращена безпека типів: Запобігає випадковому змішуванню структурно схожих, але логічно різних типів.
- Покращена ясність коду: Робить код більш читабельним та легким для розуміння, чітко розрізняючи типи.
- Зменшення кількості помилок: Виявляє потенційні помилки на етапі компіляції, зменшуючи ризик помилок під час виконання.
- Покращена підтримка: Полегшує підтримку та рефакторинг коду, забезпечуючи чітке розділення відповідальності.
Недоліки брендованих типів
- Збільшена складність: Додає складності до кодової бази, особливо при роботі з великою кількістю брендованих типів.
- Накладні витрати під час виконання: Створює невеликі накладні витрати через потребу в допоміжних функціях та валідації під час виконання.
- Потенціал для шаблонного коду: Може призвести до появи шаблонного коду, особливо при створенні та валідації брендованих типів.
Альтернативи брендованим типам
Хоча брендовані типи є потужною технікою для досягнення номінальної типізації в TypeScript, існують альтернативні підходи, які ви можете розглянути.
Непрозорі типи (Opaque Types)
Непрозорі типи схожі на брендовані типи, але надають більш явний спосіб приховати базовий тип. TypeScript не має вбудованої підтримки непрозорих типів, але їх можна симулювати за допомогою модулів та приватних символів.
Класи
Використання класів може забезпечити більш об'єктно-орієнтований підхід до визначення різних типів. Хоча класи в TypeScript мають структурну типізацію, вони пропонують чіткіше розділення відповідальності і можуть використовуватися для забезпечення обмежень за допомогою методів.
Бібліотеки, такі як `io-ts` або `zod`
Ці бібліотеки надають складну валідацію типів під час виконання і можуть поєднуватися з брендованими типами для забезпечення безпеки як на етапі компіляції, так і під час виконання.
Висновок
Брендовані типи в TypeScript — це цінний інструмент для підвищення безпеки типів та ясності коду в структурній системі типів. Додаючи "бренд" до типу, ви можете застосувати номінальну типізацію та запобігти випадковому змішуванню структурно схожих, але логічно різних типів. Хоча брендовані типи вносять певну складність і накладні витрати, переваги покращеної безпеки типів та супроводу коду часто переважують недоліки. Розгляньте можливість використання брендованих типів у сценаріях, де потрібно гарантувати, що значення належить до певного типу, незалежно від його структури.
Розуміючи принципи, що лежать в основі структурної та номінальної типізації, та застосовуючи найкращі практики, викладені в цій статті, ви зможете ефективно використовувати брендовані типи для написання більш надійного та легкого в супроводі коду на TypeScript. Від представлення валют та ідентифікаторів до забезпечення доменно-специфічних обмежень, брендовані типи надають гнучкий та потужний механізм для підвищення безпеки типів у ваших проєктах.
Працюючи з TypeScript, досліджуйте різноманітні техніки та бібліотеки, доступні для валідації та забезпечення типів. Розгляньте можливість використання брендованих типів у поєднанні з бібліотеками для валідації під час виконання, такими як io-ts
або zod
, щоб досягти комплексного підходу до безпеки типів.