HapInS Developers Blog

HapInSが提供するエンジニアリングの情報サイト

TypeScriptのタグ付きユニオン型で型安全なプログラミング

HapInSアドベントカレンダー2024、6日目! 毎朝最大5kmジョギングしていつも疲れ果てているh_shimakawaです。

今回はTypeScriptの型について記事を書きました!

はじめに

TypeScriptで、成功・失敗などの状態にで型が変わる値を扱う際、 optionalなプロパティまみれになった経験はありませんか?

特に、フロントエンド開発において非同期処理の結果を管理する場合、こうした状況に直面することが多いです。 私はreactのhooksを使った開発では、WebApiがloading|success|errorの値を返すことが多く、よく悩んでいました。  

この記事では、そのような場合に便利な「タグ付きunion型」を使った型定義の方法を紹介します。

成功と失敗で明確に型がわかるとうれしいですね!

目次

問題が発生する型定義

まずは以下のgetUserInfo()関数を見てください。 成功時と失敗時で異なる型の値を返しています。

enum Status {
  Success = "success",
  Error = "error",
}

type User = {
  id: number;
  name: string;
}

const getUserInfo = (id: number) => {
  if (id === 1) {
    // 取得成功。
    return {
      status: Status.Success,
      user: { id: 1, name: "Taro" }, // User型
    };
  } else {
    // エラー
    return {
      status: Status.Error,
      errorLog: "User not found",
    };
  }
}

上記コードの問題

この関数を使うと、以下のような問題が発生します。

const result = getUserInfo(x);
// 備考: getUserInfoの型は以下のようになっている
// type Response = {
//  status: string;
//  user?: User;
//  errorLog?: string;
// };

if(result.status === Status.Success){
  // userの型がoptionalなので、コンパイルエラーを防ぐには冗長なチェックが必要。
  doSomeThing(result.user.name); // Error: result.user is possibly 'undefined'.
}else{
  // エラー時にもuserプロパティが存在してしまうため、不適切にアクセス可能。
  console.log("error:", result.errorLog, result.user?.id);
}

問題点の整理

  • statusによる判別が可能ですが、型が明確に区別されていません。
  • プロパティがすべてoptionalになり、冗長なチェックが必要。
  • エラー時にresult.userが無効であることを型で保証できないため、型安全性ではありません。

タグ付きユニオン型とは?

タグ付きユニオン型(Tagged Union Type)は、異なる型のオブジェクトを、1つの型で型安全に扱うための方法です。 識別用のタグ(statusなど)を使うことで、各ケースの処理を型安全に記述できます。

以下に例を示します。


type SuccessResult = {
  status: Status.Success; // 成功時のタグ。Status型の中でも特定の値を示す
  user: User;
};

type ErrorResult = {
  status: Status.Error; // 失敗時のタグ。こちらも具体的な値
  errorLog: string;
};

// タグ付きユニオン型の定義
type Result = SuccessResult | ErrorResult;

この型定義により、成功時とエラー時のレスポンスを明確に区別し、それぞれの型にあるプロパティだけをアクセスできます。

const getUserInfo = (id: number): Result => {
  if (id === 1) {
    return {
      status: Status.Success,
      user: { id: 1, name: "Taro" },
    };
  } else {
    return {
      status: Status.Error,
      errorLog: "User not found",
    };
  }
}

const result = getUserInfo(1);

switch (result.status) {
  case Status.Success:
    // resultはSuccessResult型と推論され、result.userは確実に存在する
    console.log(result.user.name); // 型安全にアクセスできる
    break;
  case Status.Error:
    console.error(result.errorLog);
    // 存在しないプロパティへのアクセスは型エラーで防止される
    // console.error(result.user?.name);
    break;
}

タグ付きユニオン型の利点

  • 成功時とエラー時の型が明確に区別でき、型安全性が向上。
  • 冗長なoptionalチェックが不要。
  • エディタの補完機能が正確になるので、快適なコーディングができます。

まとめ

タグ付きユニオン型を使うことで、状態ごとに型を明確に区別でき、型安全にプログラミングができます。 特に非同期処理を扱う時に有用です。 皆さんも使ってみませんか?