はじめに
TypeScriptを書いていて、if/elseやswitch文が長くなりすぎて「これ、なんか違うよな...」と思ったことはないですか。
個人的には、状態管理やAPIレスポンスの処理で条件分岐が5つ、10つと増えていくと、もうコードを読む気力がなくなってくるんですよね。しかも、新しいケースを追加したときに「あ、このパターン忘れてた」みたいなバグが発生しがち。
そこで紹介したいのがTs-Patternです。これ、TypeScriptに本格的なパターンマッチングを導入してくれるライブラリなんですが、使ってみたらもうswitch文には戻れなくなりました。
GitHubスターは14.6kで、月間1,100万ダウンロード超え。バンドルサイズはわずか2kBで、依存関係もゼロ。正直、導入しない理由がないという話です。
特徴・メリット
1. 型安全なパターンマッチング
Ts-Patternの最大の強みは、TypeScriptの型システムと完全に統合されている点ですね。マッチした時点で型が自動的に絞り込まれるので、ハンドラ関数の中では正しい型で扱える。
2. 網羅性チェック
.exhaustive()メソッドを使うと、すべてのケースをカバーしているかコンパイル時にチェックしてくれます。これ、意外と大事で、Unionタイプに新しい型を追加したときに「ここも対応しなきゃ」とちゃんとエラーで教えてくれる。
3. 複雑なデータ構造に対応
ネストされたオブジェクト、配列、タプル、Set、Mapまで対応。深い構造のデータを扱うときでも、シンプルに書ける。
4. 軽量
バンドルサイズは約2kB(minified + gzip)。依存関係もないので、気軽に導入できます。
インストール方法
npmでサクッと入れられます。
npm install ts-pattern
yarnやpnpmを使っている人は、それぞれのコマンドで。
yarn add ts-pattern
# or
pnpm add ts-pattern
基本的な使い方
シンプルなマッチング
まずは基本形から。match関数で値を受け取って、.with()でパターンを定義していきます。
import { match } from 'ts-pattern';
type Status = 'idle' | 'loading' | 'success' | 'error';
const getMessage = (status: Status): string =>
match(status)
.with('idle', () => '待機中です')
.with('loading', () => '読み込み中...')
.with('success', () => '完了しました')
.with('error', () => 'エラーが発生しました')
.exhaustive();
.exhaustive()を付けておくと、将来Statusに新しい値が追加されたときにコンパイルエラーになる。これがめちゃくちゃ安心感ある。
オブジェクトのパターンマッチング
オブジェクトの構造でマッチングもできます。
import { match } from 'ts-pattern';
type ApiResponse =
| { type: 'success'; data: { id: number; name: string } }
| { type: 'error'; message: string }
| { type: 'loading' };
const handleResponse = (response: ApiResponse) =>
match(response)
.with({ type: 'success' }, ({ data }) => `ユーザー: ${data.name}`)
.with({ type: 'error' }, ({ message }) => `エラー: ${message}`)
.with({ type: 'loading' }, () => '読み込み中...')
.exhaustive();
ワイルドカードと述語関数
Pモジュールを使うと、より柔軟なマッチングができます。
import { match, P } from 'ts-pattern';
const validateAge = (age: number) =>
match(age)
.with(P.number.lt(0), () => '年齢は正の数で入力してください')
.with(P.number.between(0, 17), () => '未成年です')
.with(P.number.between(18, 64), () => '成人です')
.with(P.number.gte(65), () => 'シニアです')
.exhaustive();
P.number.lt()、P.number.gte()みたいな述語が用意されているのが便利ですね。
P.select()で値を抽出
深いネストから必要な値だけ取り出したいときはP.select()が使えます。
import { match, P } from 'ts-pattern';
type Event =
| { type: 'click'; payload: { x: number; y: number } }
| { type: 'keydown'; payload: { key: string } };
const handleEvent = (event: Event) =>
match(event)
.with(
{ type: 'click', payload: P.select() },
(coords) => `クリック位置: (${coords.x}, ${coords.y})`
)
.with(
{ type: 'keydown', payload: { key: P.select() } },
(key) => `キー入力: ${key}`
)
.exhaustive();
実践的なユースケース
状態管理のReducer
Reduxライクな状態管理を書くときに、Ts-Patternが真価を発揮します。
import { match, P } from 'ts-pattern';
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; message: string };
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; data: string[] }
| { type: 'FETCH_ERROR'; message: string }
| { type: 'RESET' };
const reducer = (state: State, action: Action): State =>
match([state, action] as const)
.with([P._, { type: 'FETCH_START' }], () => ({ status: 'loading' as const }))
.with([{ status: 'loading' }, { type: 'FETCH_SUCCESS', data: P.select() }],
(data) => ({ status: 'success' as const, data }))
.with([{ status: 'loading' }, { type: 'FETCH_ERROR', message: P.select() }],
(message) => ({ status: 'error' as const, message }))
.with([P._, { type: 'RESET' }], () => ({ status: 'idle' as const }))
.otherwise(() => state);
状態とアクションの組み合わせでマッチングできるので、「loadingのときにFETCH_SUCCESSが来たら」みたいな条件が自然に書ける。
フォームバリデーション
複数のバリデーションルールを組み合わせるときにも使えます。
import { match, P } from 'ts-pattern';
type ValidationResult =
| { valid: true }
| { valid: false; errors: string[] };
const validateEmail = (email: string): ValidationResult =>
match(email)
.with(P.string.includes('@').and(P.string.includes('.')),
() => ({ valid: true as const }))
.with(P.string.length(0),
() => ({ valid: false as const, errors: ['メールアドレスを入力してください'] }))
.otherwise(() => ({ valid: false as const, errors: ['メールアドレスの形式が正しくありません'] }));
まとめ
Ts-Patternを使うと、条件分岐のコードがかなりスッキリします。個人的に気に入っているポイントは:
- switch文より読みやすい: パターンを上から順に書けるので、コードの意図が伝わりやすい
- 型安全: マッチングした時点で型が絞り込まれるのが安心
- 網羅性チェック:
.exhaustive()でケース漏れを防げる - 軽量: 2kBなので導入のハードルが低い
30代になって思うのは、コードは書く時間より読む時間のほうが圧倒的に長いということ。Ts-Patternを使うと、半年後の自分が見ても「あ、こういう条件分岐ね」とすぐ理解できるコードが書ける。
複雑な条件分岐に悩んでいる人は、ぜひ試してみてください。QOL上がりますよ。