Как типизировать Result, если операция гарантированно не может завершиться ошибкой?

Как типизировать Result, если операция гарантированно не может завершиться ошибкой?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте Result<Success, Never>. Тип Never не имеет ни одного значения, поэтому создать состояние .failure для такого Result невозможно, а успешный результат сохраняет единый интерфейс Result для дальнейшей композиции.

func makeIdentifier() -> Result<String, Never> { .success("user-42") } let result = makeIdentifier() switch result { case .success(let identifier): print(identifier) }

Здесь ветка .failure не требуется: ошибка имеет тип Never, то есть недостижима.

Исторический контекст

Проблема возникает в обобщённых API, где разные операции должны иметь одинаковую форму результата, даже если некоторые из них фактически не могут завершиться ошибкой. Простое возвращение Success точнее отражает локальную семантику, но нарушает единообразие такого конвейера.

Сочетание Result с Never позволяет выразить невозможное состояние на уровне типов, не используя фиктивный тип ошибки или соглашение вроде «ошибка никогда не возвращается». Это пример моделирования допустимых состояний средствами системы типов.

Постановка проблемы

Если для гарантированно успешной операции выбрать произвольный тип ошибки, например Error, модель будет допускать состояние, которое реально невозможно. Это ухудшает читаемость API и заставляет обрабатывать или документировать фиктивный сценарий.

Если заменить результат обычным значением, тип станет семантически точнее, но функция перестанет напрямую участвовать в API, ожидающем Result. Неверный выбор приходится компенсировать адаптерами или специальными перегрузками.

Подробное решение

Result<Success, Failure> имеет два состояния: .success(Success) и .failure(Failure). При подстановке Failure == Never второе состояние невозможно, поскольку получить значение типа Never нельзя.

Это не означает, что функция автоматически становится невыбрасывающей или что любой Result можно безопасно заменить таким типом. Гарантия относится только к конкретной сигнатуре: реализация не может вернуть ошибку, не изменив контракт.

Result<Success, Never> полезен как элемент общего конвейера. Его можно преобразовать через map, а при объединении с операциями, которые действительно могут завершиться ошибкой, успешное значение можно передать дальше. Если операция сама по себе не участвует в таком конвейере, обычный Success обычно проще и выразительнее.

Не следует выбирать Never, если отказ лишь маловероятен или сейчас не предусмотрен. В этом случае нужно моделировать реальный тип ошибки: иначе добавление нового сценария сбоя потребует изменения публичного контракта и всех его потребителей.

Ситуация из практики

Внутренний конвейер подготовки данных принимает результаты этапов в едином формате Result. Один этап собирает идентификатор из уже проверенных частей и действительно не имеет отказа, поэтому его можно объявить как Result<Identifier, Never>.

Вариант с Result<Identifier, Error> формально совместим с конвейером, но допускает фиктивную ошибку и создаёт ложное ожидание отказа. Вариант с обычным Identifier проще локально, однако потребует отдельного адаптера при передаче в общий конвейер.

Выбор Result<Identifier, Never> оправдан, если единообразие конвейера важно. Если же этап используется отдельно, лучше вернуть Identifier: отсутствие лишней оболочки делает API проще, а тип Never не должен применяться только ради формальной универсальности.

Что кандидаты часто упускают

Может ли Result<Данные, Never> всё же содержать .failure?

Нет. Для .failure требуется значение типа Never, а значений этого типа не существует. Если операция стала способна завершаться ошибкой, её сигнатуру нужно изменить на соответствующий тип ошибки, например Result<Данные, ОшибкаЗагрузки>.

Нужно ли обрабатывать .failure в switch для Result<Данные, Never>?

Нет, компилятор учитывает, что Never — непорождаемый тип, и считает ветку .success исчерпывающей. Это не просто договорённость разработчика: недостижимость ошибочного состояния следует из типа.

Зачем использовать Result<Данные, Never>, если можно вернуть Данные?

Такой тип нужен не для локального усложнения простого API, а для совместимости с обобщённым кодом, который принимает Result независимо от конкретной операции. Если общей композиции нет, прямой возврат Данные обычно лучше: он не создаёт лишнюю оболочку и точнее отражает контракт.