見出し画像

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にパターンマッチングを持ち込む事で、より型安全な環境を作ろう!

普段はテック・キャリア系の記事も書いているので、ぜひ合わせてご覧ください。

おすすめ記事

参考


この記事が気に入ったらサポートをしてみませんか?