Skip to main content

Ніколи не вводьте в TypeScript

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. Без цієї анотації звуження не відбудеться.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?