はじめに
TypeScriptを書いていて、if/elseやswitch文が長くなりすぎて「これ、なんか違うよな...」と思ったことはないですか。
個人的には、状態管理やAPIレスポンスの処理で条件分岐が5つ、10つと増えていくと、もうコードを読む気力がなくなってくるんですよね。しかも、新しいケースを追加したときに「あ、このパターン忘れてた」みたいなバグが発生しがち。
そこで紹介したいのがTs-Patternです。これ、TypeScriptに本格的なパターンマッチングを導入してくれるライブラリなんですが、使ってみたらもうswitch文には戻れなくなりました。
GitHubスターは14.6kで、月間1,100万ダウンロード超え。バンドルサイズはわずか2kBで、依存関係もゼロ。正直、導入しない理由がないという話です。
とはいえ、読むより触った方が早いと思います。記事の中に、実際にブラウザ上で動かせるTs-Patternのサンプルを置いてあるので、先に挙動を見たい方はこちらからどうぞ。
特徴・メリット
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
ts-patternのサンプルを動かす
書き方の解説に入る前に、まず動くものを触ってみてください。
ts-patternはmatch()に値を渡し、.with()でパターンを並べていくだけで分岐を書けます。ここでは注文のステータス(pending / shipped / delivered / cancelled)をセレクトボックスで切り替えて、match(status)が返すメッセージがどう変わるかを確認できます。要点だけ抜き出すと次のようになります。
import { match } from 'ts-pattern';
type OrderStatus = 'pending' | 'shipped' | 'delivered' | 'cancelled';
const getOrderMessage = (status: OrderStatus): string =>
match(status)
.with('pending', () => '注文を受け付けました')
.with('shipped', () => '発送しました')
.with('delivered', () => 'お届け済みです')
.with('cancelled', () => 'キャンセルされました')
.exhaustive();
実際にブラウザ上で動かせるのが次のサンプルです。セレクトボックスで注文ステータスを切り替えるたびにmatch(status)を呼び出し直し、.with()にマッチしたメッセージを画面に表示します。
セレクトボックスの値をshippedに切り替えると、.with('shipped', ...)にマッチして表示が「🚚 発送しました」に変わります。試しにgetOrderMessage内の.with('cancelled', ...)の行を削除してみてください。OrderStatusの4パターンを網羅できなくなるため、.exhaustive()がTypeScriptの型エラーを出します。これがts-patternの網羅性チェックの威力で、switch文では気づけないケース漏れをコンパイル時に検出できます。
基本的な使い方
シンプルなマッチング
まずは基本形から。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が来たら」みたいな条件が自然に書ける。
実際にブラウザ上で動かせるのが次のサンプルです。FETCH_START・FETCH_SUCCESS・FETCH_ERROR・RESETの4つのボタンを押すと、match([state, action] as const)が現在のstateと押したactionの組み合わせをタプルとして照合し、次の状態を返します。ワイルドカードのP._を使っているので、「stateが何であってもFETCH_STARTが来たらloadingにする」という分岐も1行で書けます。
FETCH_STARTを押さずにいきなりFETCH_SUCCESSを押すと、[{ status: 'loading' }, { type: 'FETCH_SUCCESS', ... }]にマッチせず.otherwise(() => state)が呼ばれるため、状態はidleのまま変化しません。先にFETCH_STARTを押してloading状態にしてからFETCH_SUCCESSを押すと、P.select()で取り出したdataがstate.dataにそのまま入るのが分かります。
フォームバリデーション
複数のバリデーションルールを組み合わせるときにも使えます。
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: ['メールアドレスの形式が正しくありません'] }));
実際にブラウザ上で動かせるのが次のサンプルです。入力するたびにvalidateEmailを呼び直し、P.string.includes('@').and(P.string.includes('.'))という2つの述語を.and()でつないだパターンにマッチするかどうかで、OK/NGの表示を切り替えます。
入力欄から@を消してみてください。P.string.includes('@')のパターンに外れて.otherwise()側に落ち、「メールアドレスの形式が正しくありません」というエラーに切り替わります。逆に全部消して空文字にすると、P.string.length(0)のパターンにマッチして「メールアドレスを入力してください」という専用のメッセージが出ます。このように.with()を上から順に並べるだけで、優先順位付きのバリデーションルールを書けるのがts-patternの述語マッチングの強みです。
まとめ
Ts-Patternを使うと、条件分岐のコードがかなりスッキリします。個人的に気に入っているポイントは:
- switch文より読みやすい: パターンを上から順に書けるので、コードの意図が伝わりやすい
- 型安全: マッチングした時点で型が絞り込まれるのが安心
- 網羅性チェック:
.exhaustive()でケース漏れを防げる - 軽量: 2kBなので導入のハードルが低い
30代になって思うのは、コードは書く時間より読む時間のほうが圧倒的に長いということ。Ts-Patternを使うと、半年後の自分が見ても「あ、こういう条件分岐ね」とすぐ理解できるコードが書ける。
複雑な条件分岐に悩んでいる人は、ぜひ試してみてください。QOL上がりますよ。