ts-pattern для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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:                    

Рекомендаций