Библиотека neverthrow для TypeScript: Result type для обработки ошибок
1. Что это и зачем нужна
В современной TypeScript-разработке обработка ошибок часто превращается в хаос из try/catch блоков, разбросанных по всему коду. Библиотека neverthrow предлагает элегантное решение этой проблемы, вдохновлённое функциональными языками программирования, такими как Rust и Haskell. Она предоставляет тип Result<T, E>, который явно представляет успешный результат (Ok) или ошибку (Err), делая обработку ошибок частью системы типов, а не побочным эффектом.
Основная цель neverthrow — заменить неявные исключения явными типами. Вместо того чтобы писать функцию, которая может выбросить исключение в любой момент, вы возвращаете Result, который компилятор TypeScript заставит вас обработать. Это делает код более предсказуемым, безопасным и самодокументируемым. Вы больше не гадаете, какие функции могут выбросить ошибку — тип возвращаемого значения говорит вам об этом напрямую.
Библиотека особенно полезна в проектах, где важна надёжность: backend-сервисы, финансовые приложения, системы обработки данных. Она также отлично сочетается с другими функциональными концепциями, такими как Option (Maybe) типы, и может быть интегрирована с библиотеками вроде fp-ts. neverthrow — это не просто утилита, это философия написания кода, где ошибки — это данные, а не исключительные ситуации.
2. Установка
Установка neverthrow в проект TypeScript/NPM:
npm install neverthrow
Для проектов, использующих yarn:
yarn add neverthrow
Для pnpm:
pnpm add neverthrow
Библиотека написана на TypeScript и включает в себя полные типы, поэтому дополнительная установка @types не требуется. После установки вы можете импортировать основные типы и функции:
import { ok, err, Result } from 'neverthrow';
3. Быстрый старт — минимальный рабочий пример
Давайте создадим простую функцию, которая делит два числа, но возвращает Result вместо выбрасывания исключения при делении на ноль:
import { ok, err, Result } from 'neverthrow';
// Определяем тип ошибки
type DivisionError = { message: string; code: number };
// Функция, возвращающая Result
function safeDivide(a: number, b: number): Result<number, DivisionError> {
if (b === 0) {
return err({ message: 'Division by zero', code: 1001 });
}
return ok(a / b);
}
// Использование
const result = safeDivide(10, 2);
// Обработка результата
if (result.isOk()) {
console.log('Результат:', result.value); // Результат: 5
} else {
console.error('Ошибка:', result.error.message); // Никогда не выполнится
}
const errorResult = safeDivide(10, 0);
if (errorResult.isErr()) {
console.error('Ошибка:', errorResult.error.message); // Ошибка: Division by zero
}
Этот пример демонстрирует основную идею: функция всегда возвращает Result, и вызывающий код обязан проверить, успешен ли результат, прежде чем использовать значение. Компилятор TypeScript не даст вам просто так обратиться к result.value без проверки.
4. Основные методы/функции/классы
4.1. ok(value) — Создание успешного результата
Сигнатура:
function ok<T, E = never>(value: T): Ok<T, E>
Что делает: Создаёт экземпляр Ok, представляющий успешный результат с переданным значением.
Пример:
import { ok } from 'neverthrow';
const success = ok(42);
console.log(success.isOk()); // true
console.log(success.value); // 42
4.2. err(error) — Создание ошибочного результата
Сигнатура:
function err<E, T = never>(error: E): Err<T, E>
Что делает: Создаёт экземпляр Err, представляющий неудачный результат с переданной ошибкой.
Пример:
import { err } from 'neverthrow';
const failure = err(new Error('Что-то пошло не так'));
console.log(failure.isErr()); // true
console.log(failure.error.message); // "Что-то пошло не так"
4.3. map(fn) — Трансформация успешного значения
Сигнатура:
map<U>(fn: (value: T) => U): Result<U, E>
Что делает: Если результат успешный, применяет функцию fn к значению и возвращает новый Ok с результатом. Если результат ошибочный, возвращает исходную ошибку без изменений.
Пример:
import { ok, err, Result } from 'neverthrow';
const result: Result<number, string> = ok(5);
const mapped = result.map(x => x * 2);
console.log(mapped.value); // 10
const errorResult: Result<number, string> = err('Ошибка');
const mappedError = errorResult.map(x => x * 2);
console.log(mappedError.error); // "Ошибка" (не изменилась)
4.4. mapErr(fn) — Трансформация ошибки
Сигнатура:
mapErr<F>(fn: (error: E) => F): Result<T, F>
Что делает: Если результат ошибочный, применяет функцию fn к ошибке и возвращает новый Err с преобразованной ошибкой. Если результат успешный, возвращает его без изменений.
Пример:
import { ok, err, Result } from 'neverthrow';
const errorResult: Result<number, string> = err('Ошибка 404');
const mappedError = errorResult.mapErr(e => `HTTP ${e}`);
console.log(mappedError.error); // "HTTP Ошибка 404"
const successResult: Result<number, string> = ok(100);
const mappedSuccess = successResult.mapErr(e => `HTTP ${e}`);
console.log(mappedSuccess.value); // 100 (не изменилось)
4.5. andThen(fn) — Цепочка операций (flatMap/bind)
Сигнатура:
andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F>
Что делает: Если результат успешный, применяет функцию fn, которая сама возвращает Result. Позволяет строить цепочки операций, где каждая следующая операция зависит от результата предыдущей. Если на любом этапе возникает ошибка, цепочка прерывается.
Пример:
import { ok, err, Result } from 'neverthrow';
function parseNumber(s: string): Result<number, string> {
const num = Number(s);
return isNaN(num) ? err('Не число') : ok(num);
}
function doubleIfPositive(n: number): Result<number, string> {
return n > 0 ? ok(n * 2) : err('Число не положительное');
}
const result = ok('10')
.andThen(parseNumber)
.andThen(doubleIfPositive);
console.log(result.value); // 20
const badResult = ok('-5')
.andThen(parseNumber)
.andThen(doubleIfPositive);
console.log(badResult.error); // "Число не положительное"
4.6. orElse(fn) — Обработка ошибки с возвратом Result
Сигнатура:
orElse<U, F>(fn: (error: E) => Result<U, F>): Result<T | U, F>
Что делает: Если результат ошибочный, применяет функцию fn к ошибке, которая возвращает новый Result. Позволяет восстанавливаться после ошибок или преобразовывать их в другие типы.
Пример:
import { ok, err, Result } from 'neverthrow';
function getDefaultValue(error: string): Result<number, string> {
console.warn(`Используем значение по умолчанию из-за ошибки: ${error}`);
return ok(0);
}
const result = err('Не удалось загрузить данные')
.orElse(getDefaultValue);
console.log(result.value); // 0
4.7. match(okFn, errFn) — Паттерн-матчинг для Result
Сигнатура:
match<U>(okFn: (value: T) => U, errFn: (error: E) => U): U
Что делает: Принимает две функции: одну для обработки успешного значения, другую для обработки ошибки. Возвращает результат выполнения одной из них в зависимости от состояния Result. Это элегантная альтернатива if/else.
Пример:
import { ok, err, Result } from 'neverthrow';
const result: Result<number, string> = ok(42);
const message = result.match(
(value) => `Успех: ${value}`,
(error) => `Ошибка: ${error}`
);
console.log(message); // "Успех: 42"
const errorResult: Result<number, string> = err('Не удалось вычислить');
const errorMessage = errorResult.match(
(value) => `Успех: ${value}`,
(error) => `Ошибка: ${error}`
);
console.log(errorMessage); // "Ошибка: Не удалось вычислить"
4.8. unwrapOr(defaultValue) — Извлечение значения с запасным вариантом
Сигнатура:
unwrapOr(defaultValue: T): T
Что делает: Если результат успешный, возвращает значение. Если ошибочный, возвращает defaultValue. Полезно, когда нужно безопасно извлечь значение без проверки.
Пример:
import { ok, err, Result } from 'neverthrow';
const success: Result<number, string> = ok(100);
console.log(success.unwrapOr(0)); // 100
const failure: Result<number, string> = err('Ошибка');
console.log(failure.unwrapOr(0)); // 0
4.9. asyncMap(fn) — Асинхронная трансформация
Сигнатура:
asyncMap<U>(fn: (value: T) => Promise<U>): Promise<Result<U, E>>
Что делает: Асинхронный аналог map. Если результат успешный, применяет асинхронную функцию fn к значению и возвращает Promise с новым Result. Если результат ошибочный, возвращает Promise с исходным Err.
Пример:
import { ok, err, Result } from 'neverthrow';
async function fetchUserData(userId: number): Promise<string> {
// Имитация асинхронного запроса
return `User ${userId}`;
}
const result: Result<number, string> = ok(123);
const asyncResult = await result.asyncMap(fetchUserData);
console.log(asyncResult.value); // "User 123"
4.10. combine(results) — Объединение нескольких Result
Сигнатура:
function combine<T, E>(results: Result<T, E>[]): Result<T[], E>
Что делает: Принимает массив Result и возвращает один Result, содержащий массив всех успешных значений, если все результаты успешны. Если хотя бы один результат ошибочный, возвращается первая встретившаяся ошибка.
Пример:
import { ok, err, combine, Result } from 'neverthrow';
const results: Result<number, string>[] = [
ok(1),
ok(2),
ok(3)
];
const combined = combine(results);
console.log(combined.value); // [1, 2, 3]
const mixedResults: Result<number, string>[] = [
ok(1),
err('Ошибка в элементе 2'),
ok(3)
];
const failed = combine(mixedResults);
console.log(failed.error); // "Ошибка в элементе 2"
5. Реальный практический пример
Давайте построим небольшой сервис для работы с пользователями, который демонстрирует все ключевые возможности neverthrow. Этот пример включает валидацию, работу с базой данных (симулированную) и обработку ошибок.
import { ok, err, Result, combine } from 'neverthrow';
// ========== Типы данных ==========
interface User {
id: number;
name: string;
email: string;
age: number;
}
interface ValidationError {
field: string;
message: string;
}
interface DatabaseError {
code: number;
message: string;
}
type UserError = ValidationError | DatabaseError;
// ========== Валидация ==========
function validateName(name: string): Result<string, ValidationError> {
if (!name || name.trim(). Также в библиотеке
Криптография в Swift: шифрование и хеширование для iOS/macOS
Авторизация и аутентификация в PHP: полное руководство с примерами
CLI фреймворки для Rust: обзор лучших инструментов для создания утилит командной строки
Resilience4j для Java: полное руководство с примерами