読み込み中...

TypeScriptのパターンマッチを5つの手順で完璧に理解する

TypeScriptのパターンマッチのイラスト解説 TypeScript
この記事は約20分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

近年のプログラミング界隈で、TypeScriptの人気は急上昇しています。

特に、JavaScriptのスーパーセットとしての特性を持ちながら、静的型付けの強みを取り入れたこの言語は、大規模開発における安全性や保守性を向上させる役割を果たしています。

そして、TypeScriptが持つ豊富な機能の中でも、今回の主題である「パターンマッチ」は、コードの可読性や効率性を飛躍的に向上させる魅力的な要素の一つです。

本記事では、初心者向けにTypeScriptのパターンマッチの使い方を、わかりやすいサンプルコードを交えて徹底解説します。

また、注意点やカスタマイズの方法についても詳しく触れていきますので、TypeScriptの熟練者はもちろん、これからTypeScriptを学びたいという方にも有益な情報を提供できることを目指しています。

●TypeScriptとは?

TypeScriptは、Microsoftによって開発されたJavaScriptのスーパーセットとして位置付けられる言語です。

これは、TypeScriptはJavaScriptのすべての特性を含みながら、さらにそれを拡張した機能を持っていることを意味します。

○TypeScriptの基本的な特徴

TypeScriptを学ぶことで、開発者はプログラムの正確さを増すために静的型付けのような強力な機能を活用することができます。

そうした機能が、早い段階でエラーを特定し、コードの堅牢性を高めることを可能にしています。

次に、この強力な言語のいくつかの鍵となる特徴を見ていき、これらのメリットをより深く掘り下げてみましょう。

□静的型付け

TypeScriptの最も顕著な特徴は、静的型付けを採用していることです。

これにより、コンパイル時に型の不整合やエラーを検出することができ、ランタイムエラーのリスクを大幅に減少させることができます。

このコードでは、数値型の変数に文字列を代入しようとする例を表しています。

let num: number;
num = "文字列"; // エラー! string型はnumber型に代入できません。

このコードを実行すると、”エラー! string型はnumber型に代入できません。”というエラーメッセージが表示されることになります。

□インターフェースと型エイリアス

TypeScriptでは、データ構造を明確に定義するためのインターフェースや型エイリアスを提供しています。

これにより、オブジェクトの構造や関数の引数・戻り値の型を明確にし、安全なコードの記述を促進します。

例として、ユーザー情報を表すインターフェースを次のように定義できます。

interface User {
  id: number;
  name: string;
  age: number;
}

このコードでは、Userインターフェースを使って、ユーザー情報を持つオブジェクトの構造を定義しています。

□高度な型システム

ジェネリクスやユニオン型、リテラル型など、TypeScriptは多彩な型システムを持っており、これにより柔軟でありながらも強固なコードの記述が可能となっています。

文字列または数値を受け取る関数の例を紹介します。

function processInput(input: string | number) {
  // ...
}

このコードでは、processInput関数は、文字列または数値のどちらかの型を持つinput引数を受け取ることができます。

●パターンマッチとは?

パターンマッチングは、プログラミングにおける有力な技術の1つで、ある値が特定の形やパターンに一致するかどうかを確認し、それに応じて異なる処理を行うことができます。

具体的には、入力されたデータや値が期待する型や形式、さらには内容までを詳細にチェックし、その結果に基づいて適切なアクションや出力を行うことが目的です。

たとえば、商業的なアプリケーションを考えると、異なるタイプのユーザーがアクセスした場合、それぞれのユーザーのタイプに応じて異なる振る舞いをすることが必要になることが多いです。

このような場面で、パターンマッチングは非常に役立ちます。

TypeScriptにおいても、このパターンマッチングの概念は重要であり、様々なシナリオでの使用が増えています。

特に、TypeScriptは型に厳しい言語であるため、型を用いたパターンマッチングのテクニックが特に強力です。

○なぜパターンマッチが必要なのか?

TypeScriptを使用するとき、多くの開発者は型の利点を活用してコードの品質を向上させたいと考えます。

しかし、型だけでなく、データの構造や内容に関する情報も非常に重要です。ここで、パターンマッチングの真価が発揮されます。

□コードの可読性向上

条件分岐が多くなると、コードは複雑になりがちです。パターンマッチングを使用すると、それらの条件分岐を簡潔に、かつ直感的に表現することができます。

□型の安全性の確保

TypeScriptは、型に基づいたプログラミングを強化する言語です。

パターンマッチングを使用することで、想定外の型や値が渡された場合に、それを迅速に検出し、適切に処理することができます。

□効率的なデバッグ

エラーが発生した場合、どのパターンにマッチしなかったのか、どの部分で問題が発生したのかを容易に特定することができます。

□拡張性の向上

新しいパターンや条件を追加する際、既存のコードに大きな変更を加えずに、新しいパターンを追加することができます。

●TypeScriptでのパターンマッチの基本

TypeScriptにおけるパターンマッチは、ある値が特定のパターンに一致するかを確認する強力な機能です。

この機能を利用することで、複雑な条件分岐を簡潔に表現することができます。

○サンプルコード1:基本的なマッチング

TypeScriptでのパターンマッチの基本形を表すサンプルコードを紹介します。

type Shape = 
  | { kind: 'circle', radius: number }
  | { kind: 'rectangle', width: number, height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return 3.14 * shape.radius * shape.radius;
    case 'rectangle':
      return shape.width * shape.height;
  }
}

このコードでは、2種類の図形、circlerectangleを定義しています。

そして、getArea関数を用いて、各図形の面積を計算しています。

switch文の中でshape.kindによるパターンマッチを行うことで、図形の種類ごとの処理を分岐しています。

ここで、shape.radiusshape.widthshape.heightを使用して計算を行うことができるのは、TypeScriptの型システムによって、適切なプロパティの存在が保証されているからです。

このコードを実行すると、次のような結果を得られます。

例えば、getArea({ kind: 'circle', radius: 3 })を実行すると、半径が3の円の面積、つまり28.26を返します。

同様に、getArea({ kind: 'rectangle', width: 3, height: 4 })を実行すると、3×4の長方形の面積、つまり12を返します。

○サンプルコード2:値によるマッチング

TypeScriptでは、特定の値に基づいて変数の型を絞り込むためのパターンマッチングをサポートしています。

これにより、コードの安全性を高め、不要なエラーチェックを減少させることができます。

それでは、値に基づくマッチングのTypeScriptのサンプルコードを紹介します。

type Animal = { type: 'dog' | 'cat' | 'bird', name: string };

function getAnimalSound(animal: Animal): string {
  if (animal.type === 'dog') {
    return `${animal.name}は、ワンワンと鳴きます。`;
  } else if (animal.type === 'cat') {
    return `${animal.name}は、ニャーと鳴きます。`;
  } else if (animal.type === 'bird') {
    return `${animal.name}は、ピーピーと鳴きます。`;
  }
}

このコードでは、Animalという型を定義しています。

Animal型は、typeプロパティとnameプロパティを持つオブジェクトの型として定義されています。

そして、getAnimalSound関数では、animaltypeプロパティの値によって、異なる動物の鳴き声を返すロジックが実装されています。

このコードを実行すると、例えばgetAnimalSound({ type: 'dog', name: 'タロウ' })を呼び出すと、「タロウは、ワンワンと鳴きます。」という結果が得られます。

●応用:TypeScriptのパターンマッチをさらに深く知る

TypeScriptを活用する中で、基本的なパターンマッチの使い方を理解した後、さらに高度な技術として条件付きマッチングの応用を探求してみましょう。

条件付きマッチングは、一般的なマッチングよりも複雑な条件を持つマッチングを実現します。

○サンプルコード3:条件付きマッチング

下記のサンプルコードでは、TypeScriptでの条件付きマッチングの方法を表しています。

このコードでは、type Animalという型に対して、動物の名前と種類をマッチングしています。

さらに、動物が'Dog'の場合、その犬のサイズも考慮に入れる条件を加えています。

type Animal = 
  | { kind: 'Dog', size: 'Small' | 'Large' }
  | { kind: 'Cat' };

function describeAnimal(animal: Animal): string {
  switch (animal.kind) {
    case 'Dog':
      // 犬のサイズに応じて異なるメッセージを返す
      return animal.size === 'Small' 
        ? 'これは小さな犬です。'
        : 'これは大きな犬です。';
    case 'Cat':
      return 'これは猫です。';
  }
}

const myDog = { kind: 'Dog', size: 'Small' };
const description = describeAnimal(myDog);

このコードでは、switch文を使ってanimal.kindに応じた処理を行っています。

'Dog'の場合、さらにanimal.sizeの値をチェックして、小さな犬か大きな犬かを判断しています。

このコードを実行すると、myDogというオブジェクトをdescribeAnimal関数に渡し、その結果として'これは小さな犬です。'という文字列がdescription変数に格納されます。

条件付きマッチングを使用することで、より詳細な条件に基づいて異なる結果を返すことができます。

これは、多様なデータ構造や条件を持つアプリケーションを開発する際に非常に役立ちます。

○サンプルコード4:オブジェクトのパターンマッチング

TypeScriptのパターンマッチの使い方をさらに深く探求していきましょう。

今回は、オブジェクトを使ったパターンマッチングに焦点を当てて説明します。

オブジェクトは、キーと値のペアでデータを保持するデータ構造です。

TypeScriptのパターンマッチを利用することで、オブジェクトの特定のキーと値にマッチする条件を設定し、それに応じた処理を実行することができます。

下記のサンプルコードでは、オブジェクトのキー「type」の値に応じて、異なるメッセージをコンソールに出力する簡単な例を表しています。

type Action = 
  { type: 'ADD', payload: number } |
  { type: 'DELETE', payload: string } |
  { type: 'RESET' };

function handleAction(action: Action): void {
  switch(action.type) {
    case 'ADD':
      console.log(`追加する数値は${action.payload}です。`);
      break;
    case 'DELETE':
      console.log(`削除するアイテムは${action.payload}です。`);
      break;
    case 'RESET':
      console.log(`リセットが選択されました。`);
      break;
  }
}

このコードでは、Actionという型を定義しています。

この型は、3つの異なるオブジェクトの形式を持つことができます。

それぞれのオブジェクトは「type」というキーを持っており、このキーの値によってオブジェクトの形式が異なります。

handleAction関数は、引数としてAction型のオブジェクトを受け取り、switch文を使用してtypeキーの値に応じた処理を行います。

このサンプルコードを実行すると、次のようになります。

例えば、handleAction({ type: 'ADD', payload: 5 })というコードを実行すると、コンソールに「追加する数値は5です。」というメッセージが表示されます。

同様に、handleAction({ type: 'DELETE', payload: 'item1' })を実行すると、「削除するアイテムはitem1です。」というメッセージが表示され、handleAction({ type: 'RESET' })を実行すると、「リセットが選択されました。」というメッセージが表示されます。

また、オブジェクトの中に別のオブジェクトがネストされている場合も、パターンマッチングを利用して特定のキーと値にマッチする条件を設定することができます。

下記のサンプルコードは、ネストされたオブジェクトのキーと値を使用してマッチングを行う例を表しています。

type NestedAction = 
  { type: 'UPDATE', details: { field: 'name', value: string } } |
  { type: 'UPDATE', details: { field: 'age', value: number } };

function handleNestedAction(action: NestedAction): void {
  switch(action.details.field) {
    case 'name':
      console.log(`名前が${action.details.value}に更新されました。`);
      break;
    case 'age':
      console.log(`年齢が${action.details.value}歳に更新されました。`);
      break;
  }
}

このサンプルコードでは、NestedActionという型が定義されており、この型のオブジェクトは「type」と「details」という2つのキーを持っています。

「details」というキーの値はさらにオブジェクトとして、fieldvalueという2つのキーを持っています。

handleNestedAction関数は、引数としてNestedAction型のオブジェクトを受け取り、switch文を使用してdetails.fieldキーの値に応じた処理を行います。

このサンプルコードを実行すると、例えばhandleNestedAction({ type: 'UPDATE', details: { field: 'name', value: 'Taro' } })というコードを実行すると、コンソールに「名前がTaroに更新されました。」というメッセージが表示されます。

同様に、handleNestedAction({ type: 'UPDATE', details: { field: 'age', value: 30 } })を実行すると、「年齢が30歳に更新されました。」というメッセージが表示されます。

○サンプルコード5:配列のパターンマッチング

TypeScriptにおけるパターンマッチングは非常に強力なツールであり、これまでにさまざまなシナリオでのマッチング方法を解説してきました。

今回は、配列に関するパターンマッチングを取り上げます。

配列は、順序付けられた複数の要素を格納するデータ構造です。

従って、配列に対するパターンマッチングでは、その要素や要素の順序、長さなどの特性を基にしてマッチングを行います。

TypeScriptでの配列のパターンマッチングを表すサンプルコードを紹介します。

type ArrayPattern = [number, string?];

function matchArrayPattern(arr: ArrayPattern): string {
    switch (arr.length) {
        case 1:
            return `数値だけが存在: ${arr[0]}`;
        case 2:
            return `数値と文字列が存在: ${arr[0]}, ${arr[1]}`;
        default:
            return '該当するパターンはありません';
    }
}

const sampleArray1: ArrayPattern = [10];
const sampleArray2: ArrayPattern = [20, "Hello"];

console.log(matchArrayPattern(sampleArray1));
console.log(matchArrayPattern(sampleArray2));

このコードでは、配列が2つの要素、数値と文字列を持つかどうかを判断しています。

そして、それぞれのパターンに応じた結果を返しています。

具体的には、配列が1つの要素のみを持つ場合と、2つの要素を持つ場合に分けて処理を行っています。

そして、その要素の数を基にswitch文でマッチングを行い、対応するメッセージを返しています。

このコードを実行すると、次のような結果が得られます。

数値だけが存在: 10
数値と文字列が存在: 20, Hello

配列の要素の数や型に基づいて処理を分岐することができるため、このようなパターンマッチングは実際の開発でも頻繁に利用されます。

特に、APIから取得したデータの形式が異なる場合や、ユーザーからの入力値のバリデーションなど、さまざまなシーンで活用することができます。

●注意点と対処法

TypeScriptのパターンマッチングを使用する際の注意点や対処法を知っておくことは非常に重要です。

ここでは、一般的な問題点やその解決策について、サンプルコードを交えながら説明します。

○マッチング漏れの問題とその対処法

TypeScriptのパターンマッチングを使用する際、考慮しきれていないパターンがあると、マッチング漏れという問題が生じることがあります。

この問題は、意図していない動作やバグを引き起こす可能性があります。

例として、次のサンプルコードを考えます。

type Animal = "dog" | "cat" | "bird";

function getSound(animal: Animal): string {
  switch (animal) {
    case "dog":
      return "ワンワン";
    case "cat":
      return "ニャー";
    // birdのケースが欠けている
  }
}

const result = getSound("bird");

このコードでは、getSound関数はAnimal型の動物を受け取り、その動物の鳴き声を返すものとしています。

しかし、”bird”のケースが欠けているため、getSound("bird")を実行すると未定義の値が返されます。

このようなマッチング漏れを防ぐための一つの方法として、never型を利用する方法があります。

次のように修正します。

function getSound(animal: Animal): string {
  switch (animal) {
    case "dog":
      return "ワンワン";
    case "cat":
      return "ニャー";
    default:
      const exhaustiveCheck: never = animal;
      return exhaustiveCheck;
  }
}

このコードでは、全てのケースを網羅していない場合、never型への代入が発生しコンパイルエラーとなります。

これにより、マッチング漏れを早期に検出できます。

このコードを実行すると、”bird”のケースが欠けているため、コンパイルエラーが発生することが期待されます。

○パフォーマンスに関する注意点

TypeScriptのパターンマッチングは非常に強力ですが、大量のケースや複雑なマッチング条件を持つ場合、パフォーマンス上のオーバーヘッドが生じる可能性があります。

特に、ループの中で頻繁にパターンマッチングを使用する場合や、再帰的なパターンマッチングを行う場合には注意が必要です。

実際のパフォーマンスへの影響を確認するには、プロファイリングツールを使用して実行時間を計測することをおすすめします。

もし、パターンマッチングに起因するパフォーマンスの問題を発見した場合、条件の単純化や、不要なマッチングの削減などの対処法を検討するとよいでしょう。

また、高度な最適化が求められる場面では、手動での条件分岐や、専用のデータ構造を利用することで、パフォーマンスを向上させることも考えられます。

パターンマッチングを利用する場合、その強力な機能性と引き換えに、パフォーマンスに関する考慮が必要であることを忘れないようにしましょう。

●カスタマイズ方法:パターンマッチを更に便利に使う方法

パターンマッチングは、TypeScriptでのデータ構造の解析を強化する強力なツールですが、時には標準の機能だけでは足りないことがあります。

ここでは、TypeScriptのパターンマッチングをさらに効果的に使用するためのカスタマイズ方法を解説します。

○カスタムマッチャの作成

標準のパターンマッチング機能を越えて、独自の条件でマッチングを行いたい場合は、カスタムマッチャを作成することが考えられます。

このカスタムマッチャを使用すると、特定の条件を満たすデータだけを対象としたマッチングが可能となります。

例えば、特定の文字列を含むかどうかを基にマッチングを行いたい場合、次のようなカスタムマッチャを考えることができます。

// このコードでは、文字列が特定の文字列を含むかを判定するカスタムマッチャを作成しています。
function contains(target: string, value: string): boolean {
  return target.includes(value);
}

const myString = "Hello, TypeScript!";
if (contains(myString, "TypeScript")) {
  console.log("TypeScriptが含まれています。");
}

このコードを実行すると、"TypeScriptが含まれています。"と表示されます。

カスタムマッチャのcontains関数を使用することで、特定の文字列が含まれているかどうかの条件でマッチングができるようになります。

○ユーティリティ関数としての利用

TypeScriptのパターンマッチングをさらに強化するために、ユーティリティ関数を使用するアプローチも考えられます。

これにより、より複雑な条件でのマッチングや、複数のマッチング条件を組み合わせたマッチングなどが可能となります。

例として、数値が特定の範囲内にあるかどうかを判定するユーティリティ関数を考えてみましょう。

// このコードでは、数値が指定された範囲内にあるかどうかを判定するユーティリティ関数を作成しています。
function isInRange(value: number, min: number, max: number): boolean {
  return value >= min && value <= max;
}

const age = 25;
if (isInRange(age, 20, 30)) {
  console.log("年齢は20歳から30歳の間です。");
}

このコードを実行すると、"年齢は20歳から30歳の間です。"と表示されます。

ユーティリティ関数isInRangeを使用することで、数値が指定された範囲内にあるかどうかの条件でマッチングを行うことができます。

まとめ

TypeScriptの世界では、パターンマッチは非常に重要な役割を果たしています。

この記事を通じて、その基本から応用、注意点、さらにカスタマイズ方法まで、TypeScriptのパターンマッチに関する情報を詳細に解説してきました。

この記事を終えるにあたり、TypeScriptのパターンマッチは、コードの品質を向上させ、エラーを減少させるための強力なツールであることが確認できたかと思います。

初心者から経験者まで、日常のコーディングにおいて、これらのテクニックを活用して、より洗練されたアプリケーションを開発する手助けとしてください。