🎯

Dart 3.0のSealed Classによる直和型(Union Type)の実装

2025/01/08に公開

直和型とは

直和型(Union Type)は、複数の型のいずれか1つの値を取りうる型です。代数的データ型(Algebraic Data Types, ADT)の一種で、「この型はAまたはBまたはC...のいずれかである」という関係を表現します。

Dartでは、sealed classを使用して直和型を実装できます。これは特に、互いに排他的な状態や種類を表現する際に有用です。

昔はFreezedでやっていたようです。ソースコードが古いプロジェクトに入った時に見たことがありました。フィットネスとヘルスケアのアプリの開発でしか見たことなかったので試しに使ってみたいと実験してました。

https://zenn.dev/joo_hashi/articles/7f7fa134485ff7

Dart3.0からだとsealed classを使うのが推奨されているようです。

TypeScriptのもユニオン型がある。
https://typescriptbook.jp/reference/values-types-variables/union

時々使うことがあった。最近はNext.jsを使うときに使っていた。

let numberOrUndefined: number | undefined;

これがsealed class

sealed class SleepState {
  const SleepState();
}

final class Awake extends SleepState {
  const Awake();
}

final class LightSleep extends SleepState {
  const LightSleep();
}

final class DeepSleep extends SleepState {
  const DeepSleep();
}

final class REMSleep extends SleepState {
  const REMSleep();
}

main.dartで実行するときはこのように書く。

import 'sleep_state.dart';

void main() {
  // 睡眠状態に基づいて処理を分岐する例
  
  // switchを使用した分岐処理(whenの代わり)
  String handleSleepState(SleepState state) {
    return switch (state) {
      Awake() => '起床中: 活動を記録します',
      LightSleep() => '浅い睡眠: 呼吸数と心拍数を監視中',
      DeepSleep() => '深い睡眠: 体の回復中',
      REMSleep() => 'REM睡眠: 夢を見ている可能性があります'
    };
  }

  // デフォルト値を持つswitch(whenOrNullの代わり)
  String getSleepAdvice(SleepState state) {
    return switch (state) {
      Awake() => '良い一日を!',
      DeepSleep() => '静かな環境を維持することをお勧めします',
      _ => '睡眠を妨げないようにしましょう'
    };
  }

  // 特定のケースのみ処理(maybeWhenの代わり)
  bool shouldNotifyUser(SleepState state) {
    return switch (state) {
      Awake() => true,
      _ => false
    };
  }

  // 使用例
  final states = [
    const Awake(),
    const LightSleep(),
    const DeepSleep(),
    const REMSleep(),
  ];

  print('=== 睡眠トラッカーの動作例 ===\n');
  
  for (final state in states) {
    print('現在の状態:');
    print(handleSleepState(state));
    print('アドバイス: ${getSleepAdvice(state)}');
    print('通知: ${shouldNotifyUser(state) ? "有効" : "無効"}');
    print('---\n');
  }
}

実行結果

=== 睡眠トラッカーの動作例 ===

現在の状態:
起床中: 活動を記録します
アドバイス: 良い一日を!
通知: 有効
---

現在の状態:
浅い睡眠: 呼吸数と心拍数を監視中
アドバイス: 睡眠を妨げないようにしましょう
通知: 無効
---

現在の状態:
深い睡眠: 体の回復中
アドバイス: 静かな環境を維持することをお勧めします
通知: 無効
---

現在の状態:
REM睡眠: 夢を見ている可能性があります
アドバイス: 睡眠を妨げないようにしましょう
通知: 無効
---

実装例:睡眠状態の表現

sealed class SleepState {
  const SleepState();
}

final class Awake extends SleepState {
  const Awake();
}

final class LightSleep extends SleepState {
  const LightSleep();
}

final class DeepSleep extends SleepState {
  const DeepSleep();
}

final class REMSleep extends SleepState {
  const REMSleep();
}

この実装では:

  • SleepStateは基底クラスで、可能な睡眠状態の「和」を表現
  • 各サブクラスは具体的な睡眠状態を表現
  • sealedキーワードにより、これらの状態は列挙された4つに限定される
  • finalキーワードにより、各サブクラスはこれ以上継承できない

パターンマッチング

Dart 3.0のswitch式を使用して、直和型の値に対するパターンマッチングが可能です:

String handleSleepState(SleepState state) {
  return switch (state) {
    Awake() => '起床中',
    LightSleep() => '浅い睡眠',
    DeepSleep() => '深い睡眠',
    REMSleep() => 'REM睡眠'
  };
}

網羅性チェック

switch式は全てのケースを網羅する必要があります:

// コンパイルエラー:REMSleepケースが漏れている
String incomplete(SleepState state) {
  return switch (state) {
    Awake() => '起床中',
    LightSleep() => '浅い睡眠',
    DeepSleep() => '深い睡眠'
  };
}

デフォルト処理

ワイルドカードパターン _ を使用して、特定のケース以外をまとめて処理できます:

String getSleepAdvice(SleepState state) {
  return switch (state) {
    Awake() => '良い一日を!',
    DeepSleep() => '静かな環境を維持することをお勧めします',
    _ => '睡眠を妨げないようにしましょう'  // LightSleepとREMSleepの場合
  };
}

freezedとの比較

sealed classの利点

  1. 言語ネイティブ: 外部パッケージ不要
  2. シンプル: 直感的な構文
  3. 軽量: コード生成が不要
  4. switch式との相性: パターンマッチングが自然に書ける

sealed classの制約

  1. ボイラープレート: イミュータブル性やユーティリティメソッドは自前で実装
  2. 機能制限: freezedのwhen系メソッドのような便利機能は自前で実装が必要
  3. JSON変換: シリアライゼーション機能は自前で実装が必要

ユースケース

sealed classによる直和型は以下のような場合に特に有用です:

  1. 状態管理

    • アプリケーションの状態(ローディング/成功/エラー)
    • ユーザーの状態(未認証/認証済み/ブロック)
  2. 結果の表現

    • 成功/失敗の結果
    • オプショナルな値(Some/None)
  3. メッセージの種類

    • システムメッセージの種類(情報/警告/エラー)
    • ユーザーアクションの種類(作成/更新/削除)

まとめ

Dart 3.0のsealed classは、直和型を実装する強力な方法を提供します:

  • 型安全性の保証
  • コンパイル時の網羅性チェック
  • パターンマッチングによる簡潔な分岐処理

シンプルな直和型が必要な場合は、sealed classで十分です。より高度な機能(イミュータブル性、JSONシリアライゼーション、豊富なユーティリティメソッド)が必要な場合は、freezedの使用を検討してください。

Discussion