Перейти до вмісту

JavaScript і TypeScript

Мова фронтенду зсередини: типи й порівняння, область видимості та this, модель виконання з event loop і промісами, TypeScript від основ до просунутих типів. Бекенд-розробника на fullstack-співбесіді перевіряють саме тут: там, де JavaScript поводиться не так, як C#.

Junior+

Які типи є в JavaScript і чим == відрізняється від ===?

Section titled “Які типи є в JavaScript і чим == відрізняється від ===?”
Відповідь

Що це

  • Примітиви — string, number, bigint, boolean, undefined, null, symbol. Незмінні, порівнюються за значенням.
  • Об’єкти — усе інше: {}, масиви, функції, Date. Змінна зберігає посилання, як змінна класу в C#.

Як користуватися

  • Тип перевіряють через typeof: typeof 'a' — 'string'. Дві дивини: typeof null — 'object', а для функції — 'function'. Масив перевіряють через Array.isArray.
  • Порівнюють через ===: мають збігтися і тип, і значення. == перед порівнянням приводить типи, тому 0 == '' — true.
  • Falsy-значення: false, 0, '', null, undefined, NaN. Усе інше в if вважається істиною, зокрема [] і '0'.

Типова помилка

Писати count || 10 для значення за замовчуванням. При count = 0 вийде 10; потрібно count ?? 10.

Як влаштовано

  1. === — строга рівність: різні типи одразу дають false, об’єкти рівні, лише якщо це одне посилання.
  2. == — нестрога: null і undefined рівні лише одне одному, рядок і boolean приводяться до числа, об’єкт — до примітиву через valueOf або toString.
  3. NaN не дорівнює нічому, навіть собі. Перевірка — Number.isNaN(x).

Коли що обирати

Задача Що писати
Порівняти значення ===
Перевірити на null і undefined разом x == null
Замовчування лише для null і undefined x ?? def
Замовчування для будь-якого falsy x || def
Знайти NaN у масиві includes, а не indexOf

Пастки

  • typeof null === 'object'. Перевірка «це об’єкт» — typeof x === 'object' && x !== null.
  • Глобальний isNaN. Він спершу приводить аргумент до числа: isNaN('abc') — true. Потрібен Number.isNaN.
  • Порівняння об’єктів. { id: 1 } === { id: 1 } — false. Ні Equals, ні перевантаження операторів, як у C#, немає: глибоке порівняння пишуть самі або беруть із бібліотеки.
  • Дробові числа. number — 64-бітний double, як double у C#: 0.1 + 0.2 !== 0.3. Гроші рахують у копійках або передають рядком.

Правило

Завжди ===, крім x == null; для замовчувань — ??. Правило ESLint eqeqeq закріплює це в коді.

Під капотом

  • == за специфікацією — алгоритм IsLooselyEqual: за різних типів операнди крок за кроком приводяться до числа або примітиву. Об’єкт стає примітивом через Symbol.toPrimitive, valueOf або toString, тому [] == false: [] → '' → 0.
  • typeof null === 'object' — помилка першої реалізації мови: тег типу «об’єкт» був нулем, як і вказівник null. Її залишили заради сумісності.
  • Видів рівності чотири: ==, ===, Object.is (SameValue) і SameValueZero. Останнім користуються includes, Map і Set, тому NaN у них знаходиться.

Ціна

  • number — IEEE 754 double. Цілі точні до Number.MAX_SAFE_INTEGER (2^53 − 1). long-ідентифікатор із C#, більший за поріг, JSON.parse мовчки округлить: 9007199254740993 стане 9007199254740992.
  • bigint точний, але не змішується з number (1n + 1 — TypeError), а JSON.stringify його не серіалізує.

Компроміси

  • Великі long і decimal бекенд віддає рядком: у System.Text.Json — JsonNumberHandling.WriteAsString або конвертер для конкретних полів. Альтернатива — домовитися, що ID не перевищують 2^53.
  • Ключі Map і Set порівнюються за посиланням. Об’єкт-ключ шукають за тим самим екземпляром або переходять на рядковий ключ.

Як перевірити в продакшені

  • ESLint: eqeqeq, а для TypeScript — @typescript-eslint/prefer-nullish-coalescing і strict-boolean-expressions.
  • Втрату точності ID ловить інтеграційний тест з ідентифікатором, більшим за 2^53: у відповіді він має повернутися без змін.

Підсумок: рівність у JavaScript — кілька алгоритмів із різною семантикою, а найдорожча пастка на стику з .NET — точність number.

Приклад
console.log(typeof null); // object — історична помилка
console.log(typeof undefined); // undefined
console.log(typeof (() => {})); // function
console.log(0 == ''); // true: '' приводиться до 0
console.log(0 === ''); // false: різні типи
console.log(null == undefined); // true
console.log(null == 0); // false
console.log([] == false); // true: [] → '' → 0, false → 0
console.log(Boolean([])); // true: будь-який об'єкт truthy
console.log(NaN === NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log([NaN].includes(NaN)); // true
console.log([NaN].indexOf(NaN)); // -1
const qty = 0;
console.log(qty || 10); // 10: 0 — falsy
console.log(qty ?? 10); // 0: 0 — не null і не undefined
const a = { id: 1 };
const b = a;
console.log(a === b); // true: одне посилання
console.log(a === { id: 1 }); // false: інший об'єкт
Middle+

Як працюють область видимості, замикання та this?

Section titled “Як працюють область видимості, замикання та this?”
Відповідь

Що це

  • Область видимості — частина коду, де змінна доступна. let і const видно в блоці { }, var — у всій функції.
  • Замикання — функція разом зі змінними, доступними під час її створення. Вона бачить їх і після виходу із зовнішньої функції.
  • this — об’єкт «перед крапкою» під час виклику: у user.hello() це user.

Як користуватися

  • Змінні — через const, через let — якщо значення змінюється. var у новому коді не потрібен.
  • Замикання зберігає приватний стан: makeCounter() повертає функцію з власним count.
  • Колбеки — стрілковими функціями: власного this у них немає, вони беруть його ззовні.

Типова помилка

Передати метод як колбек: setTimeout(user.hello, 100). Функцію викличуть не через user, і this вказуватиме не на нього. Правильно — setTimeout(() => user.hello(), 100).

Як влаштовано

Властивість var let, const
Область функція блок
До оголошення undefined ReferenceError (TDZ)
Повторне оголошення можна помилка

Правила this за пріоритетом:

  1. new F() — новий об’єкт.
  2. f.call(obj), f.apply(obj, args), f.bind(obj) — явно заданий об’єкт. bind повертає нову функцію із закріпленим this.
  3. obj.f() — об’єкт перед крапкою.
  4. f() — undefined у strict mode (модулі та класи завжди strict), інакше глобальний об’єкт.

Стрілкова функція в цих правилах не бере участі: this вона бере з оточення під час створення, і call чи bind його не змінюють.

Пастки

  • Цикл із var. Усі колбеки ділять одну змінну й бачать її останнє значення. let у for створює нову змінну на кожну ітерацію. У C# так само поводиться for, а foreach із C# 5 створює нову змінну на ітерацію.
  • Відрив методу. const f = obj.method втрачає obj. У C# делегат Action a = obj.Method запам’ятовує об’єкт, у JavaScript — ні.
  • Стрілка як метод об’єкта. Стрілкова функція в літералі об’єкта бере this ззовні, а не сам об’єкт.

Правило

const за замовчуванням, let — якщо потрібне перевизначення. Колбеки — стрілковими функціями; метод, який віддають назовні, — полем-стрілкою або через bind.

Під капотом

  • За специфікацією функція зберігає посилання на лексичне оточення (Environment Record) місця створення. Пошук змінної йде ланцюжком оточень назовні.
  • Рушій кладе захоплені змінні в об’єкт контексту в купі. У V8 замикання однієї функції ділять один контекст: довгоживучий колбек тримає й змінні, потрібні лише сусідньому замиканню.
  • this — не змінна оточення, а параметр виклику. Стрілкові функції беруть this, arguments і super із зовнішньої функції, тому їх не можна викликати через new.

Ціна

  • Метод класу — одна функція на прототипі. Поле-стрілка handle = () => {} — окрема функція в кожному екземплярі: пам’ять на кожен об’єкт, і нащадок не викличе її через super.handle().
  • Замикання утримує все, що захопило. Невідписаний обробник події або таймер тримає компонент і його дані — частий витік пам’яті в SPA.

Компроміси

  • bind або поле-стрілка: пам’ять на екземпляр проти надійного this, коли метод віддають назовні.
  • Замикання дає справжню приватність без класів. Приватні поля #x дають її й класу, без окремої функції на екземпляр.

Як перевірити в продакшені

  • Chrome DevTools → Memory → Heap snapshot: порівняти два знімки й пройти ланцюжком Retainers. Записи (closure) і system / Context показують, яке замикання тримає пам’ять.
  • ESLint no-var, prefer-const і no-loop-func прибирають пастки з var і циклами.

Підсумок: замикання — оточення в купі, яке живе, доки жива функція, а this задає виклик; обидва — часті джерела витоків і помилок у колбеках.

Приклад
console.log(v); // undefined: var піднято, він дорівнює undefined
var v = 1;
try {
console.log(l);
} catch (e) {
console.log(e.name); // ReferenceError: l у TDZ
}
let l = 2;
const withVar = [];
for (var i = 0; i < 3; i++) withVar.push(() => i);
console.log(withVar.map(f => f())); // [ 3, 3, 3 ]: одна i на всю функцію
const withLet = [];
for (let j = 0; j < 3; j++) withLet.push(() => j);
console.log(withLet.map(f => f())); // [ 0, 1, 2 ]: нова j на кожну ітерацію
function makeCounter() {
let count = 0; // живе, доки жива повернена функція
return () => ++count;
}
const next = makeCounter();
next();
console.log(next()); // 2
Втрата this
class Counter {
count = 0;
inc() { return ++this.count; } // метод на прототипі
incArrow = () => ++this.count; // поле: власна функція в кожного об'єкта
}
const c = new Counter();
const inc = c.inc; // метод відірвано від об'єкта
const incArrow = c.incArrow;
try {
inc(); // this === undefined: класи в strict mode
} catch (e) {
console.log(e.name); // TypeError
}
console.log(incArrow()); // 1: this узято з конструктора
console.log(inc.call(c)); // 2: this задано явно
console.log(inc.bind(c)()); // 3: bind повертає нову функцію
console.log(inc.apply(c, [])); // 4: як call, аргументи масивом
Middle+

Як влаштовані event loop, проміси та async/await у JavaScript?

Section titled “Як влаштовані event loop, проміси та async/await у JavaScript?”
Відповідь

Що це

  • Один потік. Поки працює функція, нічого іншого не відбувається.
  • Event loop — цикл, який бере наступну задачу з черги, коли поточний код завершився: клік, таймер, відповідь сервера.
  • Проміс — об’єкт, який пізніше стане виконаним (fulfilled) з результатом або відхиленим (rejected) з помилкою.

Як користуватися

  1. Функція з async завжди повертає проміс.
  2. await усередині неї чекає на проміс і віддає його результат; потік тим часом вільний.
  3. Помилки ловлять звичайним try/catch навколо await.
  4. Незалежні запити запускають паралельно: await Promise.all([loadUser(), loadOrders()]).

Типова помилка

Викликати async-функцію без await і без .catch. Помилка всередині неї не потрапить у ваш try/catch і стане необробленою.

Як влаштовано

  1. Синхронний код виконується на стеку викликів до кінця.
  2. Коли стек порожній, виконуються всі мікрозадачі: колбеки then, catch і finally, продовження після await, queueMicrotask. Мікрозадачі, додані дорогою, виконуються в цьому ж проході.
  3. Браузер може відмалювати кадр.
  4. Event loop бере одну задачу з черги: setTimeout, подію, повідомлення мережі. Цикл повторюється.

Код async-функції до першого await виконується синхронно, решта стає мікрозадачею.

Коли що обирати

Метод Результат
Promise.all масив результатів; відхиляється за першої помилки
Promise.allSettled статуси всіх промісів, ніколи не відхиляється
Promise.race перший завершений — успіх або помилка
Promise.any перший успішний; якщо впали всі — AggregateError

Пастки

  • Promise.all не скасовує решту. Один проміс відхилився — інші запити й далі працюють, їхній результат просто губиться.
  • forEach з async-колбеком. forEach не чекає на проміси. Послідовно — for...of з await, паралельно — Promise.all з map.
  • Зайва послідовність. Два незалежні await поспіль чекають суму часу, а не максимум.

Правило

Кожен проміс або чекають через await, або повертають, або явно обробляють через .catch. Незалежні операції — через Promise.all.

Під капотом

  • Специфікація ECMAScript описує реакції промісів як завдання (jobs), а браузер і Node.js кладуть їх у чергу мікрозадач. У Node.js є ще черга process.nextTick, вона виконується раніше за проміси, і фази циклу libuv: таймери, введення-виведення, setImmediate.
  • async-функція — машина станів, як у C#. Але продовження після await завжди йде через чергу мікрозадач, навіть якщо проміс уже виконано; у C# await завершеної Task продовжує синхронно.
  • Введення-виведення виконують браузер і libuv у своїх потоках, у JavaScript повертаються лише колбеки.

Ціна

  • Усе — в одному потоці. Обчислення на 200 мс заморожує інтерфейс або, на Node.js, усі запити сервера. CPU-роботу виносять у Web Worker або worker_threads.
  • Блокувань немає, але гонки між await є: відповіді двох запитів пошуку приходять в іншому порядку, і стара перезаписує нову.

Компроміси

  • Скасування в промісу немає. Його передають через AbortController і AbortSignal — аналог CancellationToken: fetch(url, { signal }), AbortSignal.timeout(5000).
  • Promise.all — fail-fast, зазвичай це те, що потрібно. allSettled — коли частковий результат кращий за жодний.

Як перевірити в продакшені

  • Unhandled rejection: у браузері подію unhandledrejection надсилають у моніторинг помилок, Node.js з версії 15 за замовчуванням падає.
  • Довгі задачі видно в DevTools → Performance. Затримку event loop у Node.js вимірює perf_hooks.monitorEventLoopDelay().
  • Правило @typescript-eslint/no-floating-promises знаходить проміси без обробки.

Підсумок: один потік і порядок «задача → усі мікрозадачі → кадр» дають просту модель без блокувань, але будь-яка довга синхронна робота зупиняє все.

Приклад
console.log('A'); // 1
setTimeout(() => console.log('timeout'), 0); // 7: задача
Promise.resolve().then(() => console.log('then')); // 4: мікрозадача
queueMicrotask(() => console.log('microtask')); // 5: мікрозадача
async function run() {
console.log('run start'); // 2: до await — синхронно
await null;
console.log('after await'); // 6: продовження — мікрозадача
}
run();
console.log('B'); // 3

Числа в коментарях — порядок виведення.

Комбінатори промісів
// ES-модуль: await на верхньому рівні
const ok = (value, ms) =>
new Promise(resolve => setTimeout(() => resolve(value), ms));
const fail = (message, ms) =>
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
try {
await Promise.all([ok(1, 50), fail('boom', 10)]);
} catch (e) {
console.log(e.message); // boom: через 10 мс, на ok(1) не чекаємо
}
const settled = await Promise.allSettled([ok(1, 10), fail('boom', 10)]);
console.log(settled.map(r => r.status)); // [ 'fulfilled', 'rejected' ]
console.log(await Promise.any([fail('a', 5), ok(2, 20)])); // 2: перший успішний
await Promise.race([fail('a', 5), ok(2, 20)])
.catch(e => console.log(e.message)); // a: перший завершений
try {
await Promise.any([fail('a', 5), fail('b', 5)]);
} catch (e) {
console.log(e.constructor.name, e.errors.length); // AggregateError 2
}
Middle+

Що дає TypeScript і як ним правильно користуватися?

Section titled “Що дає TypeScript і як ним правильно користуватися?”
Відповідь

Що це

TypeScript — надбудова над JavaScript: та сама мова плюс анотації типів. Компілятор tsc перевіряє типи й видає JavaScript без них.

Навіщо

  • Помилки знаходяться під час збирання: одрук у властивості, забутий аргумент, можливий undefined.
  • IDE знає типи: автодоповнення й безпечне перейменування.

Як користуватися

  • Описуйте форму даних через interface або type: interface User { id: number; name: string }.
  • Параметри функцій анотуйте, а тип змінної компілятор часто виводить сам: const n = 5 — number.
  • Тримайте "strict": true у tsconfig.json; із TypeScript 6.0 це поведінка за замовчуванням.

Типова помилка

Ставити any, коли компілятор свариться. any вимикає перевірку, і помилка переїжджає в рантайм. Для значення невідомого типу є unknown.

Як влаштовано

  • Структурна типізація. Об’єкт підходить під interface Point, якщо в нього є x і y. У C# типізація номінальна: два класи з однаковими полями несумісні.
  • Звуження. Компілятор стежить за перевірками: після typeof x === 'string' змінна x має тип string. Працюють typeof, instanceof, in, === і type guard — функція з результатом x is User.
  • Generics схожі на C#: function first<T>(items: T[]), обмеження через extends. Але в рантаймі T немає: new T() і typeof T неможливі.

Коли що обирати

Задача Вибір
Форма об’єкта, контракт класу interface, розширюється через extends
Union, кортеж, обчислюваний тип type
Значення невідомого типу unknown і звуження
Міграція з JavaScript, іншого виходу немає any, точково

Пастки

  • Перевірка зайвих властивостей працює лише для об’єктного літерала. Той самий об’єкт, переданий через змінну, пройде.
  • as — не приведення. data as User у рантаймі нічого не перевіряє.
  • strict — ще не все. Доступ за індексом arr[0] дає T, а не T | undefined, доки не ввімкнено noUncheckedIndexedAccess.

Правило

strict завжди, unknown замість any, as — лише там, де ви знаєте більше за компілятор, і з коментарем чому.

Під капотом

  • tsc — перевірка типів плюс виведення JavaScript. Збирачі esbuild і SWC, а також Node.js 22.18+ лише вирізають типи, не перевіряючи їх, тому tsc --noEmit — окремий крок CI.
  • Вирізати можна не все: enum, namespace з кодом і parameter properties породжують JavaScript. Прапорець erasableSyntaxOnly (TypeScript 5.8) забороняє такий синтаксис.
  • Типів у рантаймі немає, тому DI в Angular і NestJS спирається на класи як значення. Інтерфейс не може бути токеном: для нього заводять InjectionToken або рядковий токен.

Ціна

  • Відповідь API типізована рівно настільки, наскільки їй повірили: res.json() повертає Promise<any>. Помилка формату проявиться далеко від місця отримання.
  • Перевірка схемою в рантаймі коштує CPU і розміру бандла, зате помилка падає на межі зі зрозумілим шляхом до поля.

Компроміси

  • Zod (або Valibot, ArkType): схема — єдине джерело правди, тип виводиться через z.infer. Генерація типів з OpenAPI дає контракт без перевірки в рантаймі.
  • Структурна типізація плутає UserId і OrderId, якщо обидва — string. Branded-тип string & { readonly __brand: 'UserId' } повертає номінальність.
  • TypeScript 7 (липень 2026) — компілятор, переписаний на Go: перевірка приблизно в 10 разів швидша.

Як перевірити в продакшені

  • У CI: tsc --noEmit і ESLint з @typescript-eslint/no-explicit-any та правилами no-unsafe-*.
  • Помилки валідації схеми пишуть у лог зі шляхом до поля: вони першими показують, що бекенд змінив контракт.

Підсумок: TypeScript перевіряє код, а не дані, тому на межі із зовнішнім світом потрібна перевірка в рантаймі.

Приклад
interface Point { x: number; y: number }
class Vector { x = 3; y = 4; } // про Point нічого не знає
function length(p: Point): number {
return Math.hypot(p.x, p.y);
}
console.log(length(new Vector())); // 5: підходить за формою
console.log(length({ x: 6, y: 8 })); // 10
function describe(value: unknown): string {
if (typeof value === 'string') return value.toUpperCase(); // value: string
if (Array.isArray(value)) return `array of ${value.length}`; // value: any[]
return 'other';
}
console.log(describe('ts')); // TS
console.log(describe([1, 2])); // array of 2
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // n: number | undefined
console.log(n?.toFixed(1)); // 1.0
Перевірка відповіді сервера
// npm i zod
import { z } from 'zod';
const User = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof User>; // { id: number; name: string }
const raw: unknown = JSON.parse('{"id":"42","name":"Ann"}');
const trusted = raw as User; // компілюється: as нічого не перевіряє
console.log(typeof trusted.id); // string
const result = User.safeParse(raw);
if (result.success) {
console.log(result.data.id);
} else {
console.log(result.error.issues[0].path); // [ 'id' ]
}
Senior+

Просунуті типи TypeScript: discriminated unions, utility, mapped і conditional types — коли вони потрібні?

Section titled “Просунуті типи TypeScript: discriminated unions, utility, mapped і conditional types — коли вони потрібні?”
Відповідь

Що це

  • Union A | B — значення одного з типів. Discriminated union — union об’єктів зі спільним полем, яке в кожному варіанті має своє літеральне значення: status: 'draft', status: 'paid'.
  • Utility types — вбудовані «функції над типами»: приймають тип і повертають новий.

Як користуватися

  • Перевірте поле-мітку в if або switch — усередині гілки TypeScript знає точний варіант: у гілці 'paid' доступне paidAt.
  • Partial<T> — усі поля необов’язкові: тіло PATCH-запиту.
  • Pick<T, 'a' | 'b'> і Omit<T, 'id'> — вибрати або прибрати поля: форма створення без id.
  • Record<K, V> — словник: Record<string, number>.

Типова помилка

Описувати стан необов’язковими полями: { status: string; paidAt?: string }. Компілятор не знає, коли paidAt заповнене, і перевірки розповзаються кодом.

Як влаштовано

  • keyof T — union імен властивостей, T[K] — тип властивості за ключем, typeof value — тип значення.
  • Mapped type { [K in keyof T]: ... } перебирає ключі й будує новий об’єктний тип. Модифікатори ?, readonly і -? додають або знімають ознаки, as перейменовує ключі. Так влаштовані Partial, Readonly, Record.
  • Conditional type T extends U ? X : Y обирає тип за умовою. infer R витягує частину типу: T extends Promise<infer R> ? R : T. На цьому побудовані ReturnType, Awaited, Exclude.
  • Перевірка через never. У гілці default після всіх case тип звужено до never. Рядок const x: never = order перестає компілюватися, щойно з’являється новий варіант.
  • satisfies (TypeScript 4.9) перевіряє, що значення підходить під тип, але залишає виведений тип: keyof typeof routes — 'home' | 'user', а не string. У проєктах на Angular 13 (TypeScript 4.4–4.6) його ще не було.

Пастки

  • Omit<User, 'idd'> з одруком компілюється: ключі Omit не перевіряються, а Pick їх перевіряє.
  • Partial неглибокий: вкладені об’єкти залишаються обов’язковими.
  • Conditional type над union розподіляється за варіантами: ToArray<string | number> дає string[] | number[]. Щоб цього не було, обидві сторони обгортають у кортеж: [T] extends [U].

Правило

Стани — discriminated union з перевіркою через never; похідні форми — utility types від одного вихідного типу; власні mapped і conditional types — коли вбудованих не вистачає.

Під капотом

  • Ці типи існують лише для компілятора й не породжують коду. Кожне застосування generic-типу — інстанціювання, яке компілятор обчислює й кешує.
  • Рекурсія має межу: за надто глибокого інстанціювання — помилка TS2589 «Type instantiation is excessively deep and possibly infinite».

Ціна

Складні generic-типи сповільнюють перевірку й підказки IDE, а повідомлення про помилки розгортаються на кілька екранів.

Компроміси

  • Генерація з OpenAPI. ASP.NET Core (Microsoft.AspNetCore.OpenApi, .NET 9+) видає документ, openapi-typescript генерує з нього типи, а NSwag або Orval — одразу клієнт. Типи не розходяться з бекендом, але точні настільки, наскільки описано API: без [ProducesResponseType] або TypedResults схема відповіді буде неповною.
  • Згенеровані проти власних. Згенеровані DTO — транспорт. Доменні стани на клієнті краще описати discriminated union і відобразити в них відповідь.
  • Виразність проти читабельності. Тип на 20 рядків з conditional і mapped types зрозумілий лише авторові. Публічні контракти модулів тримають простими.

Як перевірити в продакшені

  • tsc --extendedDiagnostics показує кількість інстанціювань і час перевірки, --generateTrace — які типи дорогі.
  • Тести типів: @ts-expect-error або expectTypeOf у Vitest фіксують поведінку власних утиліт.
  • CI перегенеровує типи з OpenAPI й падає на git diff --exit-code, якщо контракт змінився без фронтенду.

Підсумок: просунуті типи окуповуються на межах — у станах і контрактах API, — а платять за них часом компіляції та читабельністю.

Приклад
type Order =
| { status: 'draft' }
| { status: 'paid'; paidAt: string }
| { status: 'shipped'; trackingNo: string };
function label(order: Order): string {
switch (order.status) {
case 'draft':
return 'Draft';
case 'paid':
return `Paid at ${order.paidAt}`; // order звужено до варіанта 'paid'
case 'shipped':
return `Track ${order.trackingNo}`;
default: {
const unreachable: never = order; // новий статус — помилка компіляції тут
return unreachable;
}
}
}
console.log(label({ status: 'paid', paidAt: '2026-10-10' })); // Paid at 2026-10-10
console.log(label({ status: 'shipped', trackingNo: 'UA1' })); // Track UA1
Utility, mapped і conditional types
interface User { id: number; name: string; email: string }
type UserPatch = Partial<Omit<User, 'id'>>; // { name?: string; email?: string }
type UserById = Record<number, User>; // словник id → User
type Getters<T> = {
[K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};
type NameGetter = Getters<Pick<User, 'name'>>; // { getName: () => string }
type Unwrap<T> = T extends Promise<infer R> ? R : T;
type Loaded = Unwrap<Promise<User[]>>; // User[]
const routes = {
home: '/',
user: '/users/:id',
} satisfies Record<string, string>;
type RouteName = keyof typeof routes; // 'home' | 'user'
const patch: UserPatch = { name: 'Ann' };
console.log(Object.keys(patch)); // [ 'name' ]
console.log(routes.user); // /users/:id

З анотацією const routes: Record<string, string> тип RouteName був би просто string.