ts-patternを使ってTypeScriptでもパターンマッチングを導入しよう!
こんにちは、てりーです。
普段はFEエンジニアとして活動しています。(詳しいプロフィールはこちら)
ts-patternとは?
TypeScriptで使用されるパターンマッチングライブラリです。
用いる事で型安全な条件分岐が書けます!
2024年現在でTypeScriptにパターンマッチングを導入しようとした時に、最も推奨される選択肢かなと思っています。
パターンマッチングとは
Haskell、Scala、Elixirなどの関数型プログラミングでよく用いられる考え方です。
データ構造に応じて、パターン毎の書き分けが可能です!
TypeScriptでも関数型プログラミングを行うケースが増えてきたので、パターンマッチングを導入する機会も増えてきました。
メリット①:型の追加に強い!
パターンマッチングを使うと、型への要素の追加に対するパターン(条件分岐)の書き漏れを未然に防いでくれます!
ts-patternとswitch文を使った例で見ていきましょう。
Petの種類に応じた鳴き声をgetSoundで処理しています!
まず最初の実装では「dog、cat、bird」のパターンを書いています。
ts-pattern
import { match } from 'ts-pattern';
type Pet = 'dog' | 'cat' | 'bird';
function getSound(pet: Pet): string {
return match(pet)
.with('dog', () => 'Bark!')
.with('cat', () => 'Meow!')
.with('bird', () => 'Tweet!')
.exhaustive(); // すべてのケースを処理しないとエラーになる
}
console.log(getSound('dog')); // "Bark!"
console.log(getSound('cat')); // "Meow!"
console.log(getSound('bird')); // "Tweet!"
switch文
type Pet = 'dog' | 'cat' | 'bird';
function getSound(pet: Pet): string {
switch (pet) {
case 'dog':
return 'Bark!';
case 'cat':
return 'Meow!';
default:
return 'Unknown sound';
}
}
console.log(getSound('dog')); // "Bark!"
console.log(getSound('cat')); // "Meow!"
console.log(getSound('bird')); // "Unknown sound"
今の所、どちらも問題のないコードです!
(個人的にはこの段階で可読性の観点からts-patternの方が好き)
ここで仕様追加がありPetにfishが追加される事となりました!!
その際の挙動でts-patternとswitch文には挙動の差が出ます。
ts-pattern:getSoundに追加されたfishに対する処理を書かないと、getSound使用時にエラーが出る
例:
type Pet = 'dog' | 'cat' | 'bird' | 'fish'; // 新しいペットを追加
function getSound(pet: Pet): string {
return match(pet)
.with('dog', () => 'Bark!')
.with('cat', () => 'Meow!')
.with('bird', () => 'Tweet!')
.with('fish', () => 'Blub!') // ここの追加が必要
.exhaustive();
}
switch文:型追加してもgetSound使用時にはdefaultに流れる為、処理の書き漏れを検知できない場合がある
メリット②:可読性が高い
ts-pattarnによるパターンマッチングは個人的にメチャクチャ読みやすいです!
他に条件分岐が行われるif-elseやswitch文と比べるとネストが深くなりにくく、直感的にパターンを把握しやすいです。
getMessageという関数に対する条件分岐の書きっぷりで比較してみましょう!
if-elseの例
function getMessage(status: string): string {
if (status === 'loading') {
return 'Loading...';
} else if (status === 'success') {
return 'Success!';
} else if (status === 'error') {
return 'An error occurred.';
} else {
return 'Unknown status';
}
}
console.log(getMessage('success')); // "Success!"
console.log(getMessage('error')); // "An error occurred."
console.log(getMessage('unknown')); // "Unknown status"
switch文の例
function getMessage(status: string): string {
switch (status) {
case 'loading':
return 'Loading...';
case 'success':
return 'Success!';
case 'error':
return 'An error occurred.';
default:
return 'Unknown status';
}
}
console.log(getMessage('success')); // "Success!"
console.log(getMessage('error')); // "An error occurred."
console.log(getMessage('unknown')); // "Unknown status"
ts-patternの例
import { match } from 'ts-pattern';
function getMessage(status: 'loading' | 'success' | 'error' | 'unknown'): string {
return match(status)
.with('loading', () => 'Loading...')
.with('success', () => 'Success!')
.with('error', () => 'An error occurred.')
.otherwise(() => 'Unknown status');
}
console.log(getMessage('success')); // "Success!"
console.log(getMessage('error')); // "An error occurred."
console.log(getMessage('unknown')); // "Unknown status"
比較まとめ
if-else: 条件が少ない場合には使いやすいが、ネストが深くなりがちで、冗長になる。条件分岐が増えると可読性が低下。
switch: ある程度シンプルな代替手段だが、型の安全性や拡張性に乏しい。大規模な条件分岐では見づらくなる。
ts-pattern: 条件分岐が直感的に記述でき、複雑な場合でもシンプルなコードで可読性を保つ。型の安全性と拡張性が高く、新しい条件の追加時にエラーが発生しやすいため、バグを防止しやすい。
まとめ
今回はts-patternの導入メリットを中心に解説した為、基本形のみ紹介させて頂きました。
TypeScriptにパターンマッチングを持ち込む事で、より型安全な環境を作ろう!
普段はテック・キャリア系の記事も書いているので、ぜひ合わせてご覧ください。
おすすめ記事
参考
この記事が気に入ったらサポートをしてみませんか?