На 7-ом уроке мы разберем тип, который часто вызывает вопросы у новичков, но становится мощным инструментом для написания надежного кода. Это тип never и его отличия от void.
Что такое тип never и зачем он нужен?
Давай начнем с самого главного, с определения. Тип never в TypeScript представляет тип значений, которых никогда не возникает. Прочитай это предложение еще раз. Это не просто «что-то, что мы не используем» или «отсутствие значения». Это фундаментальное понятие, которое говорит компилятору: «В этой точке кода выполнение функции не завершится нормально. Сюда мы никогда не дойдем».
Ты можешь спросить: «Максим, зачем мне нужно описывать то, чего никогда не случится?». И вопрос будет абсолютно законным. Ответ кроется в одной из главных целей TypeScript, статистическом анализе кода и выводе типов. Компилятор постоянно строит граф возможных путей выполнения твоего кода и проверяет их на типобезопасность. Тип never это краеугольный камень в этой системе. Он позволяет компилятору точно знать, что определенные ветки кода недостижимы, что делает проверки более строгими, а твой код более предсказуемым.
Представь себе охранную систему в здании. never это как датчик, который срабатывает не тогда, когда кто-то прошел, а тогда, когда кто-то прошел через стену. Это событие, которое по логике вещей невозможно и система должна на него отреагировать. В нашем случае «отреагировать» выдаст ошибку компиляции или наоборот, корректно сузить типы в других частях кода.
В каких ситуациях тип never появляется сам по себе?
TypeScript умный язык. Он не заставляет тебя постоянно указывать never вручную. В большинстве случаев он выводит этот тип сам, когда анализирует код и понимает, что функция не может завершиться нормально. Есть несколько классических сценариев, когда это происходит.
Первый и самый очевидный сценарий это функция, которая всегда выбрасывает исключение. Как только выполняется оператор throw, нормальное выполнение функции прекращается. У нее нет шансов вернуть какое-либо значение.
function throwError(message: string): never { throw new Error(message); } // Компилятор понимает, что результат вызова этой функции нельзя присвоить ни одному типу, кроме never let result: never = throwError("Всё сломалось!"); // А вот это уже вызовет ошибку компиляции: // let numberResult: number = throwError("Попытка присвоить never числу"); // Ошибка!
Второй классический пример, функция с бесконечным циклом, из которого нет выхода. Такая функция также никогда не завершится и не вернет значение.
function infiniteLoop(): never { while (true) { console.log("Бесконечно вечный..."); // Здесь нет break или return, цикл действительно бесконечный } }
Третий сценарий, более продвинутый, но очень важный, исчерпывающие проверки (exhaustiveness checks) в конструкциях switch или цепочках if-else. Это тот случай, где never показывает свою настоящую силу для повышения надежности кода.
Допустим, у нас есть тип объединение (union type) Status, который может принимать одно из трех значений. Мы пишем функцию-обработчик для каждого статуса.
type Status = "success" | "error" | "pending"; function handleStatus(status: Status) { switch (status) { case "success": console.log("Успех!"); break; case "error": console.log("Ошибка!"); break; case "pending": console.log("В процессе..."); break; default: // Готово let unexpectedStatus: never = status; throw new Error(`Неизвестный статус: ${unexpectedStatus}`); } }
Сейчас в default мы присваиваем status переменной типа never. Это безопасно, потому что компилятор знает, что к моменту выполнения default все возможные варианты для Status уже были обработаны, а значит, status теоретически не может иметь никакого значения и его тип внутри default — never. Это работает без проблем.
Но что произойдет, если мы расширим наш тип Status, добавив новое значение, например, "rejected"?
type Status = "success" | "error" | "pending" | "rejected"; // Добавили новый статус function handleStatus(status: Status) { switch (status) { case "success": console.log("Успех!"); break; case "error": console.log("Ошибка!"); break; case "pending": console.log("В процессе..."); break; // Забыли обработать case "rejected" default: // Теперь компилятор видит, что в status может прийти "rejected", тип которого string. // Мы пытаемся присвоить string (возможный тип status) переменной с типом never. let unexpectedStatus: never = status; // ОШИБКА КОМПИЛЯЦИИ! // Тип 'string' не может быть присвоен типу 'never'. throw new Error(`Неизвестный статус: ${unexpectedStatus}`); } }
Компилятор укажет на ошибку! Он говорит нам: «Эй, дружище, твоя проверка не является исчерпывающей! Ты добавил новый статус, но не обработал его в switch, поэтому теперь в default может прийти значение, а это небезопасно». Это невероятно мощный механизм, который заставляет тебя явно обрабатывать все возможные случаи, предотвращая ошибки в рантайме. По сути, мы переносим потенциальную ошибку из времени выполнения в время компиляции, когда ее исправить гораздо дешевле и проще.
Что такое тип void?
Прежде чем углубляться в различия, давай четко вспомним, что такое void. В отличие от never, void описывает ситуацию, когда функция завершается нормально, но не возвращает никакого значимого значения. Она делает свою работу и просто заканчивается.
Классический пример, функция, которая что-то выводит в консоль или меняет состояние DOM-дерева.
function logMessage(message: string): void { console.log(message); // Здесь нет return, поэтому возвращаемое значение undefined. // Но тип именно void, а не undefined, потому что это семантически другое. } function updateDOMElement(element: HTMLElement, text: string): void { element.innerText = text; // Функция завершилась успешно, свою работу сделала, значения не вернула. }
Важное техническое отличие: в JavaScript функция без return возвращает undefined. TypeScript для подобных случаев использует тип void, чтобы явно разделить семантику «функция возвращает undefined как значение» (что можно типизировать как (): undefined) и «функция не предназначена для возврата значения» ((): void).
Ключевые различия между never и void
Теперь, armed with знаниями о обоих типах, давай проведем четкую линию между ними. Это самое главное, что тебе нужно вынести из этого урока.
-
Семантика (Суть):
-
void: Функция завершилась нормально, но не вернула значимое значение. Выполнение кода продолжается после вызова этой функции. -
never: Функция не завершилась нормально. Выполнение кода никогда не продолжится после вызова этой функции. Она либо сломала программу (выбросила исключение), либо ушла в бесконечность (зависла в цикле).
-
-
Возвращаемое значение:
-
void: Функция на самом деле возвращаетundefinedпри выполнении в JavaScript. -
never: Функция ничего не возвращает в принципе. У нее нет конечной точки выхода.
-
-
Использование в типах:
-
void: Переменная типаvoidможет быть присвоена толькоundefinedилиnull(если не включенstrictNullChecks). Но в основном он используется для указания типа возвращаемого функцией значения. -
never: Типneverявляется подухом (subtype) всех типов в системе TypeScript. Это значит, что его можно присвоить куда угодно:let x: number = throwError();. С точки зрения типобезопасности это допустимо, потому что ошибка выбросится и присвоениеxникогда не произойдет. Однако, никуда нельзя присвоить значение типаnever(кроме самогоnever). Он стоит на вершине иерархии как «ничто».
-
-
Поведение в объединениях (Union Types) и пересечениях (Intersection Types):
-
neverв объединениях исчезает.string | neverупрощается доstring. Потому что объединение с «ничем» не меняет исходный тип. -
neverв пересечениях «поглощает» другие типы.string & neverупрощается доnever. Потому что невозможно найти значение, которое одновременно является иstringиnever. -
voidведет себя как обычный тип в этих операциях.string | voidэто корректный union-тип.string & voidэто пересечение, которое на практике почти никогда не встречается.
-
Практические примеры и задачи для закрепления
Давай перейдем от теории к практике. Чтобы закрепить материал, я приготовил для тебя несколько задач и примеров. Обязательно попробуй их выполнить самостоятельно в своей IDE или в TypeScript Playground.
Задача 1: Фабрика ошибок
Напиши функцию createError, которая принимает код ошибки (число) и сообщение (строку). Функция должна бросать исключение, созданное из этих данных. Явно укажи для функции возвращаемый тип never. Убедись, что компилятор правильно определяет тип.
// Твое решение: function createError(code: number, message: string): never { throw { code, message }; } // Проверка: console.log("Это сообщение не будет выведено"); // Компилятор может даже подсветить эту строчку как недостижимую после вызова функции ниже? createError(404, "Not Found"); console.log("И это тоже");
Задача 2: Бесконечный обработчик
Представь, ты пишешь программу для сервера, который должен слушать входящие подключения в бесконечном цикле. Создай функцию startServer, которая внутри содержит while (true) и логирует сообщение «Сервер слушает на порту 3000» каждые 5 секунд. Тип функции укажи как never.
// Твое решение: function startServer(): never { while (true) { console.log("Сервер слушает на порту 3000"); // Для задержки используй setTimeout, но помни, что это не остановит цикл // Эта задача больше про типы, чем про асинхронность :) // В реальности так, конечно, не делают из-за блокировки event loop. const waitUntil = new Date().getTime() + 5000; while (new Date().getTime() < waitUntil) {} } } // Вызов функции: // startServer(); // console.log("Сервер запущен?"); // Эта строка никогда не выполнится
Задача 3: Исчерпывающая проверка (Exhaustiveness Check)
Дан тип Shape и функция calculateArea, которая вычисляет площадь для каждой фигуры. Добавь новую фигуру Triangle в тип Shape. Твоя задача добиться, чтобы компилятор TypeScript указал на ошибку в блоке default, тем самым заставив тебя обработать новый case.
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "rectangle"; width: number; height: number }; // | { kind: "triangle"; base: number; height: number }; // Раскомментируй эту строку function calculateArea(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; case "rectangle": return shape.width * shape.height; default: // Добейся, чтобы здесь была ошибка компиляции после добавления Triangle const _exhaustiveCheck: never = shape; return _exhaustiveCheck; } } // После того как ты добавишь Triangle и получишь ошибку, допиши обработку для треугольника: // case "triangle": // return 0.5 * shape.base * shape.height;
Распространенные ошибки и лучшие практики
-
Ошибка: Использовать
neverдля обычных функций, которые могут завершиться. Это вводит компилятор в заблуждение и ломает анализ кода. -
Лучшая практика: Всегда используй
neverдля функций, которые гарантированно бросают исключения или входят в бесконечный цикл. Это делает твои намерения явными для компилятора и других разработчиков. -
Лучшая практика: Обязательно применяй технику исчерпывающих проверок с
neverвdefaultблокахswitchпри работе с union-типами. Это твой надежный страж, который не даст тебе пропустить добавление новой логики при расширении типов. -
Ошибка: Путать
voidиneverв типах колбэков. Если колбэк не должен быть прерван исключением, используйvoid. Если же колбэк может бросить исключение (и это часть его логики), то его тип, скорее всего, будет() => void, а не() => never, так как бросок исключения это только один из возможных сценариев.
Понимание разницы между never и void это признак того, что ты переходишь от просто написания кода на TypeScript к мышлению на нем. Ты начинаешь видеть код так, как его видит компилятор, а это бесценный навык.
Это был урок 7 из моего комплексного курса по TypeScript для начинающих. Если ты хочешь пройти весь путь от основ до продвинутых тем под моим руководством, жду тебя на полном курсе!
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


