Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Ніколи не вводьте в TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`never`** - це нижній тип (bottom type) у TypeScript: жодне значення цього типу не може існувати під час виконання програми. ```typescript function fail(msg: string): never { throw new Error(msg); // ніколи не повертається } type Status = 'ok' | 'error'; // const check: never = s у default ловить необроблені члени union ``` **Ключове:** `never` для шляхів, які не завершуються (throw, нескінченний цикл, недосяжна гілка), а не для функцій без повернення (для них є `void`).Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`never`** - це нижній тип (bottom type) у TypeScript: значення цього типу не може існувати під час виконання програми. ## Теорія ### TL;DR - `never` означає "цей шлях у коді неможливий" - компілятор вважає його глухим кутом - Аналогія: дорога з знаком "немає виїзду". Жодне значення не виходить з іншого боку. - `void` = функція завершилась, але нічого не повернула. `never` = функція взагалі не завершується. - `const x: never = value` у гілці `default` switch - спіймає необроблені члени union на етапі компіляції - `never` зникає в union-типах: `string | never` = `string` ### Швидкий приклад ```typescript // Кидає виняток замість повернення - анотується як never function fail(message: string): never { throw new Error(message); } // Перевірка на вичерпність discriminated union type Status = 'ok' | 'error' | 'pending'; function describe(s: Status): string { switch (s) { case 'ok': return 'Все добре'; case 'error': return 'Щось пішло не так'; case 'pending': return 'Ще чекаємо'; default: const check: never = s; // помилка компіляції, якщо Status отримає новий член throw new Error('Необроблений: ' + check); } } ``` Додай новий член до `Status`, не оновивши switch, і компілятор видасть помилку саме на рядку з `check`. Баг знаходиться до того, як код потрапить у продакшен. ### Головна різниця з void `void` каже: функція дійшла до кінця і нічого корисного не повернула. Повернути `undefined` - цілком нормально. `never` каже: функція взагалі не досягне кінця - вона кинула виняток, зациклилась назавжди або пішла шляхом, який компілятор довів як недосяжний. З гілки `never` не виходить жодне значення. ### Коли використовувати - Допоміжні функції, що кидають помилку: `function fail(msg: string): never { throw new Error(msg); }` - Вичерпна перевірка switch на discriminated union: `const x: never = value` у гілці `default` - Нескінченні цикли, які дійсно не повертаються: воркери, polling-цикли - Фільтрація типів у conditional types: `T extends never ? ... : ...` прибирає неможливі типи Не варто використовувати `never` просто для позначення "порожнього" або "нічого". Для цього є `void` або `undefined`. ### Як це обробляє компілятор TypeScript під час аналізу потоку керування (control flow analysis) відстежує кожен досяжний шлях у коді. Після `throw`, після нескінченного циклу, або після того як усі члени union звужені - TypeScript позначає решту коду як `never`. Це мітка виключно на рівні компіляції. Node.js і V8 її не бачать: тип стирається разом з усіма іншими анотаціями до запуску. Коли починаєш працювати з великою кодовою базою, де union-типи регулярно змінюються, exhaustive-перевірка через `never` стає найнадійнішим способом знаходити кожен switch, який потребує оновлення. ### Типові помилки **Помилка 1: анотування порожнього масиву як `never[]`** ```typescript const items: never[] = []; // неправильно - сюди не можна нічого додати const items: string[] = []; // правильно ``` Масив типу `never[]` відхиляє будь-який `push` і ініціалізацію зі значенням. TypeScript відмовить у будь-якому елементі, бо тип елемента є неможливим. **Помилка 2: спроба повернути значення з `never`-функції** ```typescript function crash(): never { throw new Error('down'); return 'oops'; // помилка компіляції - недосяжний код } ``` Кожен шлях у функції з анотацією `never` повинен кидати виняток або циклюватися. Оператор `return` порушує цей контракт. **Помилка 3: `never` у union в очікуванні якогось ефекту** ```typescript type ID = string | number | never; // те саме що: string | number ``` `never` поглинається будь-яким union-типом. Щоб прибрати конкретний член, використовуй `Exclude<T, U>`. **Помилка 4: `never` замість `void` для функцій без повернення** ```typescript function log(msg: string): never { // помилка - функція завершується нормально console.log(msg); } function log(msg: string): void { // правильно console.log(msg); } ``` Функція, яка просто логує і завершується, є `void`, не `never`. ### Де зустрічається в реальних проектах - **React-компоненти**: перевірка на вичерпність у switch по variant пропсах - `const check: never = variant` у `default` - **Zod**: при невдалому парсингу схема викликає функцію з типом `never`, щоб сигналізувати зупинку виконання - **Redux Toolkit**: `PayloadAction<never>` для actions без payload - **tRPC**: помилки процедур типізуються як `never`, щоб TypeScript знав: результат не буде доступний ### Питання на співбесіді **Q:** Яка різниця між `never` і `void`? **A:** `void` дозволяє `undefined` і означає функцію, яка нормально завершилась. `never` означає, що функція взагалі не досягне кінця. Жодне значення, навіть `undefined`, не можна присвоїти `never`. **Q:** Коли TypeScript сам виводить `never`? **A:** Після `throw`, після нескінченного циклу і після того як усі члени union звужені. Коли нічого не залишилось - тип стає `never`. **Q:** Що відбувається з `never` у union-типі? **A:** Він зникає. `string | never` - це просто `string`. Писати `never` у звичайному union не має жодного ефекту. **Q:** Як працює exhaustive-перевірка через `never`? **A:** Присвоюєш значення у гілці `default` змінній типу `never`. Якщо додаєш новий член union і не обробляєш його, TypeScript бачить, що змінна більше не є `never`, і видає помилку компіляції. **Q:** Що таке `asserts never` і де воно зустрічається? **A:** Це предикат типу на assertion-функціях. Функція з типом `(x: unknown): asserts x is never` повідомляє TypeScript: після її виклику значення, що передавалось, вважається неможливим і виключається з подальших перевірок. ## Приклади ### Вичерпна перевірка варіантів у компоненті ```typescript type AlertVariant = 'success' | 'error' | 'warning'; function getAlertClass(variant: AlertVariant): string { switch (variant) { case 'success': return 'bg-green-500'; case 'error': return 'bg-red-500'; case 'warning': return 'bg-yellow-500'; default: // Додай 'info' до AlertVariant без оновлення цього switch - // і цей рядок одразу дасть помилку компіляції const exhausted: never = variant; throw new Error(`Необроблений варіант: ${exhausted}`); } } ``` Коли колега додає `'info'` до `AlertVariant` і забуває оновити функцію, компілятор вказує на гілку `default` одразу. Баг з'являється під час компіляції, а не в браузері користувача. ### Функція-помічник з типом never як будівельний блок ```typescript function failWith(message: string): never { throw new Error(message); } function assertDefined<T>(value: T | null | undefined, field: string): T { if (value == null) { throw new Error(`${field} є обов'язковим`); } return value; } // Завдяки never TypeScript звужує token до string const token = user.token ?? failWith('Відсутній токен'); // ^? string (не string | undefined) ``` Тип `never` на `failWith` каже TypeScript: права частина `??` завжди зупиняє виконання. Тому `token` виводиться як `string`, а не `string | undefined`. Без цієї анотації звуження не відбудеться.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.