ts-pattern для TypeScript: полное руководство с примерами
Библиотека ts-pattern привносит в TypeScript мощный и выразительный механизм сопоставления с образцом (pattern matching), вдохновлённый такими языками, как OCaml, Rust, Haskell и Scala. Вместо громоздких цепочек if-else или switch-case, ts-pattern позволяет описывать логику ветвления декларативно, проверяя структуру и типы данных. Это особенно ценно при работе с алгебраическими типами данных (ADT), такими как discriminated unions (размеченные объединения), опциональными значениями (Option) или результатами (Result).
Зачем это нужно? Во-первых, код становится более читаемым и лаконичным. Вместо того чтобы разбирать сложные вложенные объекты с помощью множества проверок, вы описываете желаемую структуру данных, а библиотека сама находит подходящий вариант. Во-вторых, ts-pattern обеспечивает исчерпывающую проверку (exhaustiveness check): если вы обработали не все возможные случаи, TypeScript выдаст ошибку на этапе компиляции. Это кардинально снижает количество багов, связанных с пропущенными состояниями. В-третьих, библиотека полностью типизирована и интегрируется с системой типов TypeScript, что делает рефакторинг безопасным и предсказуемым.
ts-pattern — это не просто замена switch. Это полноценный инструмент для декомпозиции данных, который позволяет сопоставлять не только значения, но и их типы, структуры, диапазоны и даже пользовательские предикаты. Библиотека активно используется в проектах, где важна надёжность и выразительность: от фронтенд-приложений на React/Redux до бэкенд-сервисов на Node.js.
Установка
Установка библиотеки производится через менеджер пакетов npm (или yarn/pnpm). Убедитесь, что в вашем проекте используется TypeScript версии 4.0 или выше.
npm install ts-pattern
После установки библиотека готова к использованию. Никаких дополнительных настроек tsconfig.json не требуется.
Быстрый старт
Начнём с классического примера — обработки результата HTTP-запроса. Представим, что у нас есть размеченное объединение (discriminated union), описывающее состояние загрузки данных.
import { match } from 'ts-pattern';
// Определяем возможные состояния
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: Error };
// Функция, которая возвращает текст в зависимости от состояния
function getStatusMessage(state: State): string {
return match(state)
.with({ status: 'idle' }, () => 'Ожидание...')
.with({ status: 'loading' }, () => 'Загрузка...')
.with({ status: 'success' }, ({ data }) => `Данные: ${data}`)
.with({ status: 'error' }, ({ error }) => `Ошибка: ${error.message}`)
.exhaustive(); // проверяем, что все случаи обработаны
}
// Примеры использования
console.log(getStatusMessage({ status: 'idle' })); // "Ожидание..."
console.log(getStatusMessage({ status: 'loading' })); // "Загрузка..."
console.log(getStatusMessage({ status: 'success', data: 'Привет, мир!' })); // "Данные: Привет, мир!"
console.log(getStatusMessage({ status: 'error', error: new Error('404') })); // "Ошибка: 404"
Этот пример демонстрирует основную идею: мы сопоставляем объект state с различными шаблонами. Каждый шаблон — это частичное описание структуры объекта. Если структура совпадает, выполняется соответствующая функция. Метод .exhaustive() гарантирует, что мы не забыли обработать ни один из вариантов.
Основные методы и функции
Библиотека предоставляет несколько ключевых функций и методов. Рассмотрим их подробно.
1. match(value)
Сигнатура: match<I, O>(value: I): Match<I, O>
Что делает: Создаёт контекст сопоставления для заданного значения. Все последующие вызовы .with() будут проверять это значение на соответствие шаблонам.
import { match } from 'ts-pattern';
const result = match(42)
.with(42, () => 'Это ответ на главный вопрос')
.otherwise(() => 'Что-то другое');
console.log(result); // "Это ответ на главный вопрос"
2. .with(pattern, handler)
Сигнатура: .with<P extends Pattern<I>>(pattern: P, handler: (value: Extract<I, P>) => O): Match<I, O>
Что делает: Добавляет ветку сопоставления. Если значение соответствует pattern, выполняется handler, которому передаётся суженное (narrowed) значение. Можно вызывать многократно для добавления нескольких веток.
import { match } from 'ts-pattern';
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number };
function area(shape: Shape): number {
return match(shape)
.with({ kind: 'circle' }, ({ radius }) => Math.PI * radius * radius)
.with({ kind: 'rectangle' }, ({ width, height }) => width * height)
.exhaustive();
}
console.log(area({ kind: 'circle', radius: 5 })); // 78.53981633974483
3. .when(predicate, handler)
Сигнатура: .when<P extends I>(predicate: (value: I) => value is P, handler: (value: P) => O): Match<I, O>
Что делает: Сопоставляет значение с помощью пользовательской функции-предиката (type guard). Полезно, когда нужно проверить сложное условие, которое нельзя выразить простым шаблоном.
import { match } from 'ts-pattern';
function describeNumber(n: number): string {
return match(n)
.when((x): x is number => x > 0, () => 'Положительное')
.when((x): x is number => x < 0, () => 'Отрицательное')
.otherwise(() => 'Ноль');
}
console.log(describeNumber(5)); // "Положительное"
console.log(describeNumber(-3)); // "Отрицательное"
console.log(describeNumber(0)); // "Ноль"
4. .otherwise(handler)
Сигнатура: .otherwise(handler: (value: I) => O): O
Что делает: Завершает цепочку сопоставления и возвращает результат. Выполняется, если ни один из предыдущих шаблонов не подошёл. Аналог default в switch.
import { match } from 'ts-pattern';
const greeting = match('friend')
.with('boss', () => 'Здравствуйте, шеф!')
.otherwise((name) => `Привет, ${name}!`);
console.log(greeting); // "Привет, friend!"
5. .exhaustive()
Сигнатура: .exhaustive(): O
Что делает: Завершает цепочку сопоставления, требуя, чтобы все возможные варианты значения были обработаны. Если какой-то вариант пропущен, TypeScript выдаст ошибку компиляции. Это ключевая функция для обеспечения типобезопасности.
import { match } from 'ts-pattern';
type Color = 'red' | 'green' | 'blue';
function getHex(color: Color): string {
return match(color)
.with('red', () => '#FF0000')
.with('green', () => '#00FF00')
.with('blue', () => '#0000FF')
.exhaustive(); // Если забыть 'blue', будет ошибка компиляции
}
console.log(getHex('red')); // "#FF0000"
6. P.select(name?)
Сигнатура: P.select(name?: string): PatternSelect
Что делает: Позволяет извлечь часть данных из сопоставляемого объекта по имени. Это удобно, когда нужно получить доступ к вложенным значениям, не прибегая к деструктуризации в обработчике.
import { match, P } from 'ts-pattern';
type User = { id: number; name: string; role: 'admin' | 'user' };
function getUserInfo(user: User): string {
return match(user)
.with({ role: 'admin', name: P.select('adminName') }, ({ adminName }) => `Админ: ${adminName}`)
.with({ role: 'user', name: P.select('userName') }, ({ userName }) => `Пользователь: ${userName}`)
.exhaustive();
}
console.log(getUserInfo({ id: 1, name: 'Alice', role: 'admin' })); // "Админ: Alice"
7. P.when(predicate)
Сигнатура: P.when<T>(predicate: (value: T) => boolean): PatternWhen<T>
Что делает: Создаёт шаблон, который проверяет значение с помощью произвольной функции-предиката. Отличается от метода .when() тем, что используется внутри шаблона .with() для более точного сопоставления.
import { match, P } from 'ts-pattern';
function categorizeNumber(n: number): string {
return match(n)
.with(P.when((x) => x > 0), () => 'Положительное')
.with(P.when((x) => x < 0), () => 'Отрицательное')
.otherwise(() => 'Ноль');
}
console.log(categorizeNumber(10)); // "Положительное"
8. P.not(pattern)
Сигнатура: P.not<T>(pattern: Pattern<T>): PatternNot<T>
Что делает: Создаёт шаблон, который соответствует любому значению, кроме указанного. Полезно для обработки всех случаев, кроме одного или нескольких.
import { match, P } from 'ts-pattern';
function handleEvent(event: 'click' | 'hover' | 'scroll'): string {
return match(event)
.with('click', () => 'Клик')
.with(P.not('click'), (e) => `Не клик, а ${e}`)
.exhaustive();
}
console.log(handleEvent('hover')); // "Не клик, а hover"
9. P.union(...patterns)
Сигнатура: P.union<T>(...patterns: Pattern<T>[]): PatternUnion<T>
Что делает: Создаёт шаблон, который соответствует любому из перечисленных шаблонов. Аналог логического ИЛИ для шаблонов.
import { match, P } from 'ts-pattern';
function isWeekend(day: string): boolean {
return match(day)
.with(P.union('saturday', 'sunday'), () => true)
.otherwise(() => false);
}
console.log(isWeekend('saturday')); // true
console.log(isWeekend('monday')); // false
10. P.array(pattern)
Сигнатура: P.array<T>(pattern?: Pattern<T>): PatternArray<T>
Что делает: Создаёт шаблон для сопоставления с массивом. Можно указать шаблон для каждого элемента. Если шаблон не указан, соответствует любому массиву.
import { match, P } from 'ts-pattern';
function processArray(arr: (number | string)[]): string {
return match(arr)
.with(P.array(P.number), (nums) => `Числа: ${nums.join(', ')}`)
.with(P.array(P.string), (strs) => `Строки: ${strs.join(', ')}`)
.otherwise(() => 'Смешанный массив');
}
console.log(processArray([1, 2, 3])); // "Числа: 1, 2, 3"
console.log(processArray(['a', 'b'])); // "Строки: a, b"
console.log(processArray([1, 'a'])); // "Смешанный массив"
Реальный практический пример
Рассмотрим более сложный пример — реализацию конечного автомата (state machine) для управления заказом в интернет-магазине. Заказ может находиться в одном из нескольких состояний, каждое из которых имеет свои данные и возможные переходы.
import { match, P } from 'ts-pattern';
// Определяем возможные состояния заказа
type OrderState =
| { status: 'pending'; createdAt: Date }
| { status: 'confirmed'; confirmedAt: Date; paymentMethod: string }
| { status: 'shipped'; shippedAt: Date; trackingNumber: string }
| { status: 'delivered'; deliveredAt: Date; rating?: number }
| { status: 'cancelled'; cancelledAt: Date; reason: string };
// Действия, которые можно выполнить над заказом
type OrderAction =
| { type: 'confirm'; paymentMethod: string }
| { type: 'ship'; trackingNumber: string }
| { type: 'deliver'; rating?: number }
| { type: 'cancel'; reason: string };
// Функция для применения действия к состоянию
function transition(state: OrderState, action: OrderAction): OrderState {
return match([state, action] as const)
// Подтверждение заказа (только из 'pending')
.with(
[{ status: 'pending' }, { type: