Коли схожі оголошення типів TypeScript повторюються, хочеться обчислити результат із вхідного типу. Важливіша за скорочення тексту зрозумілість зв’язків типів для читача. На прикладі уявного сервісу сповіщень про доставку розглянемо умовні типи й infer, відокремлюючи їх від перевірки справжньої правильності зовнішніх даних.
1. Умовний тип — розгалуження з типом на вході
Офіційна документація умовних типів пояснює вибір результату за умовою extends. Приклад створює булевий літеральний тип залежно від того, чи є тип рядковим. Це не функція перевірки рядка під час виконання.
type IsText<T> = T extends string ? true : false;
type NameIsText = IsText<'parcel'>; // true
type CountIsText = IsText<3>; // false
Уявімо, що функція сповіщення отримує рядковий ідентифікатор, а внутрішня обробка — числовий. До застосування умовного типу слід пояснити, навіщо їх розрізняти. Якщо значення різні, допоможуть окремі назви; за відмінності лише запису зрозумілішим може бути єдиний вхідний формат. Не ховайте неясність моделі даних за прийомами типізації.
Під час перевірки запишіть очікувані входи й результати парами. Малі приклади рядків, чисел, об’єднань і несподіваних входів полегшать читання складного оголошення. Зіставте результат у коментарі з виведеним редактором типом. Природна назва типу не гарантує очікуваного обчислення.
2. Розподіл об’єднання змінює сенс масиву
Якщо параметр типу стоїть безпосередньо ліворуч від умови, вона розподіляється на кожного члена об’єднання. Щоб розглядати його цілком, порівняйте варіант із обгортанням у квадратні дужки.
type SeparateLists<T> = T extends unknown ? T[] : never;
type Together<T> = [T] extends [unknown] ? T[] : never;
type A = SeparateLists<string | number>; // string[] | number[]
type B = Together<string | number>; // (string | number)[]
const a: A = ['a', 'b'];
const b: B = ['a', 1];
Перше — масив лише рядків або лише чисел. Друге дозволяє рядки й числа в одному масиві. Вибір залежить від того, чи весь список доставок має містити один вид кодів, чи кожен елемент може мати інший вид. В обох випадках видно дужки, але контракти масивів різні.
Зіткнувшись із проблемою, замість усього складного допоміжного типу подавайте по одному члену. Порівняйте результати рядка, числа та їх об’єднання. З’ясувавши, чи задумано розподіл, можна пояснити додавання дужок. У записі перевірки залиште не тільки результат, а й приклади дозволених значень.
3. infer називає тип усередині структури
В умовному типі infer дає ім’я частині зіставленої структури. Невеликий приклад знімає один рівень Promise.
type OneResult<T> = T extends Promise<infer Value> ? Value : T;
type Parcel = OneResult<Promise<{ tracking: string }>>;
type ParcelList = OneResult<Promise<string[]>>;
Код не запускає асинхронної роботи й не чекає на неї. Він лише виражає форму результату на етапі компіляції. Назва на кшталт Resolved може створити враження, ніби значення вже надійшло, тому враховуйте контекст використання типу. Очікування під час виконання все одно має обробляти асинхронний код.
Якщо потрібен тип рекурсивно очікуваного результату, перед розширенням одноетапного прикладу перегляньте стандартний допоміжний тип Awaited. Простота навчального типу й вимоги спільного інструмента відрізняються. Додаючи утиліту для команди, опишіть входи з відмінним від стандартного результатом та потребу в такій відмінності.
4. Поєднувати назви подій шаблонними літералами
Типи шаблонних літералів виражають комбінації рядкових літералів як тип. Приклад для доставки — назви подій із ділянки сервісу та дії.
type Scope = 'parcel' | 'address';
type Action = 'created' | 'updated';
type EventName = `${Scope}:${Action}`;
const event: EventName = 'parcel:created';
Друкарську помилку можна знайти під час компіляції, але чи всі комбінації справді чинні в сервісі — інше питання. Якщо, наприклад, підтримується лише подія видалення адреси, простий добуток ділянок і дій може неточно передавати правила. Спершу розгляньте явне зіставлення дозволених подій та payload.
Зі зростанням кількості подій важливо, чи легко читач знаходить справжні назви. Замість багатьох рівнів автоматичних комбінацій можна зібрати назви для зовнішніх систем в одному файлі. Порівняйте, як новачок команди помітить неправильну подію до й після абстракції. Точність типів і зручність оцінюйте разом.
5. Зовнішній JSON перевіряють під час виконання
Надання відповіді API типу User або Parcel не гарантує фактичної відповідності структури. Приймімо зовнішнє значення як unknown і перевірмо потрібні поля.
function hasTracking(value: unknown): value is { tracking: string } {
return typeof value === 'object' && value !== null
&& 'tracking' in value && typeof value.tracking === 'string';
}
const incoming: unknown = { tracking: 'DEMO-001' };
if (hasTracking(incoming)) console.log(incoming.tracking);
Це перевіряє лише те, що tracking — рядок. Не підтверджує існування накладної чи право користувача бачити цю доставку. Звуження типів, бізнес-правила й перевірка прав — різні обов’язки. Офіційний розділ Narrowing пояснює звуження відомостей про тип за допомогою умов.
6. Невеликий порядок вибору інструментів типізації
Спочатку запишіть два дозволені та два відхилені реальні значення. Потім виразіть сенс звичайним об’єднанням і явним оголошенням об’єкта. Якщо повторювані зв’язки залишаються, причина умовного типу чи infer буде зрозумілішою. Якщо час читання сильно зріс, можна виділити лише спільне, а решту лишити звичайними типами.
Нарешті, окремо виконуйте перевірку типів і перевірку під час виконання. Відхилення компілятором хибної назви події доповнює перевірку пояснення помилки програмою, коли сервер пропускає поле. Лише поєднавши результати малих прикладів зі справжніми межами вводу, перетворення типів допомагатимуть безпечно проєктувати застосунок.