Dart言語で型安全で使いやすい擬似的なUnion型を実装する方法

Dart
この記事はこんな人にオススメ
  • Dart言語でUnion型を実装したい人
  • Dart言語でUnion型の実装方法がわからない人
  • Dart言語でfreezedパッケージを使用したUnion型の実装方法を知りたい人

どうも、Shinyaです。

この記事では、Dart言語で型安全で使いやすい擬似的なUnion型を実装する方法について書いていこうと思います。

TypeScriptなどの言語でサポートされているUnion型は非常に便利で、次のような単純な型定義で非常に柔軟な使い方ができます。

// 文字列または数値を受け取る型
type StringOrNumber = string | number;

// Union型の関数
function printValue(value: StringOrNumber) {
    console.log(value);
}

// 使用例
printValue("Hello");  // 文字列OK
printValue(42);       // 数値もOK

しかし、Dart言語ではUnion型が言語仕様としてサポートされていないので、TypeScriptなどでサポートされているようなUnion型を実装しようとすると、擬似的にUnion型を再現するための工夫が必要になります

Dart言語で擬似的なUnion型を実装できるようになると実装の幅が広がるので、是非この記事を参考にDart言語でUnion型を試してみてください。

Dart言語の開発リポジトリでは何年も前からUnion型をDart言語に組み込むかどうかの議論が行われていますが、Dart言語の開発チームはUnion型の追加に非常に消極的なので、近いうちにUnion型がDart言語の言語仕様として実装されることはないと考えられます。

事前準備

この記事に書いていく内容を実践するためには、まずDart言語のインストールが必須となります。そのため、Dart言語をまだインストールしていない方は、以下の記事を参考にDart言語をインストールしてください。

Dart言語で擬似的なUnion型を実装していく

それでは、早速Dart言語でUnion型を実装していきましょう。Dart言語でUnion型を実装する主な方法は次の通りです。

  • sealedクラスを使用する
  • freezedパッケージを使用する

上記の2パターンについて、この記事では参考までに次の構造を持つ簡単なUnion型を実装していきます。

  • 支払方法(Payment)
    • クレジットカード(Credit
      • カード番号(cardNumber)
    • ペイパル(PayPal)
      • メールアドレス(email)
    • 銀行(Bank)
      • 顧客番号(accountNumber)

sealedクラスを使用してUnion型を実装する

sealedクラスを使用したUnion型の実装は非常に軽量で、freezedなどの追加のパッケージの追加は必要ありません

selaedクラスを使用したUnion型の実装のポイントは次の通りです。

  1. Union型として扱いたいオブジェクトをsealedクラスとして定義(e.g. Payment)
  2. Union型の要素として扱いたいオブジェクトで上記のsealedクラスをimplements
  3. 2.で実装したクラスを1.で実装したクラスのfactoryコンストラクタとして追加

例えば、次のように実装できます。

/// 支払方法に関するUnion型
sealed class Payment {
  const factory Payment.credit(final String cardNumber) = Credit;
  const factory Payment.payPal(final String email) = PayPal;
  const factory Payment.bank(final String accountNumber) = Bank;
}

/// クレジットカード
final class Credit implements Payment {
  /// カード番号
  final String cardNumber;

  const Credit(this.cardNumber);
}

/// PayPal
final class PayPal implements Payment {
  /// メールアドレス
  final String email;

  const PayPal(this.email);
}

/// 銀行
final class Bank implements Payment {
  /// 顧客番号
  final String accountNumber;

  const Bank(this.accountNumber);
}

上記で実装したPaymentオブジェクトの使い方は次の通りです。Dartのパターンマッチング構文を使用して簡単にそれぞれの型を判別することができます。

import 'package:dart_union/dart_sealed_union.dart';

void main(List<String> arguments) {
  final payments = <Payment>[
    const Payment.credit('123-4567-890'),
    const Payment.payPal('xyz@example.com'),
    const Payment.bank('xxxx-yyyy-zzzz'),
  ];

  for (final payment in payments) {
    switch (payment) {
      case Credit():
        print('Credit: ${payment.cardNumber}');
      case PayPal():
        print('PayPal: ${payment.email}');
      case Bank():
        print('Bank: ${payment.accountNumber}');
    }
  }
}

上記の検証用のmain関数を実行すると、次のようにsealedクラスで実装したUnion型がパターン毎に振り分けられることを確認できます。

Credit: 123-4567-890
PayPal: xyz@example.com
Bank: xxxx-yyyy-zzzz

freezedパッケージを使用してUnion型を実装する

freezedパッケージを使用したUnion型の実装は、sealedクラスを使用する場合と比較して難易度がやや高いです。しかし、freezedパッケージはコード生成時にテスト済みの便利なユーティリティを自動で実装してくれるのでプロジェクトの規模に関わらず積極的に活用すると良いでしょう。

freezedパッケージを使用したUnion型の実装のポイントは次の通りです。

  1. 必須のパッケージをpubspec.yamlの依存性に追加
  2. freezedを使用してオブジェクトを作成するファイルに次のインポートを追加
    • import 'package:freezed_annotation/freezed_annotation.dart';
  3. freezedを使用してオブジェクトを作成するファイルに次の定義を追加
    • part '任意のファイル名.freezed.dart';
    • 上記の「任意のファイル名」にはfreezedオブジェクトを定義したファイル名と同一の値を指定してください
  4. Union型の要素として扱いたいオブジェクトを任意の形式で実装
  5. Union型として扱いたいオブジェクトをfreezedテンプレートに従って実装
  6. freezedを使用してコード生成するためコマンドプロンプトで次のコマンドを実行
    • dart run build_runner build --delete-conflicting-outputs

例えば、次のように実装できます。

念のために補足しておくと、事前にpubspec.yamlの依存性に次のパッケージを追加してください。それぞれのパッケージのバージョンは実装時に利用可能な任意のもので大丈夫です。

name: dart_union
environment:
  sdk: ^3.9.0

dependencies:
  freezed_annotation: ^3.1.0

dev_dependencies:
  freezed: ^3.2.0
  build_runner: ^2.7.0
import 'package:freezed_annotation/freezed_annotation.dart';

part 'dart_freezed_union.freezed.dart';

/// 支払方法に関するUnion型
@freezed
abstract class Payment with _$Payment {
  const factory Payment.credit({required Credit data}) = PaymentCredit;
  const factory Payment.payPal({required PayPal data}) = PaymentPayPal;
  const factory Payment.bank({required Bank data}) = PaymentBank;
}

/// クレジットカード
final class Credit {
  final String cardNumber;

  const Credit(this.cardNumber);
}

/// PayPal
final class PayPal {
  final String email;

  const PayPal(this.email);
}

/// 銀行
final class Bank {
  final String accountNumber;

  const Bank(this.accountNumber);
}

freezedbuild_runnerを使用したコード生成が必須なので、コマンドプロンプトで次のコマンドを実行してください。

dart run build_runner build --delete-conflicting-outputs

上記のコマンドを実行すると、次の画像のようにfreezedが実行に必要なコードを自動生成したことを確認できます。

上記で実装したPaymentオブジェクトの使い方は次の通りです。sealedクラスを使用して実装した場合と同様に、Dartのパターンマッチング構文を使用して簡単にそれぞれの型を判別することができます。

import 'package:dart_union/dart_freezed_union.dart';

void main(List<String> arguments) {
  final payments = <Payment>[
    const Payment.credit(data: Credit('123-4567-890')),
    const Payment.payPal(data: PayPal('xyz@example.com')),
    const Payment.bank(data: Bank('xxxx-yyyy-zzzz')),
  ];

  for (final payment in payments) {
    switch (payment) {
      case PaymentCredit():
        print('Credit: ${payment.data.cardNumber}');
      case PaymentPayPal():
        print('PayPal: ${payment.data.email}');
      case PaymentBank():
        print('Bank: ${payment.data.accountNumber}');
    }
  }
}

また、freezedを使用した場合には、デフォルトで次の例のように.when.whenOrNullといった便利なユーティリティが利用できます

freezedを使用した場合はデフォルトで.when.whenOrNullといったユーティリティが自動生成されますが、Dart言語が3.0系に更新された際に同様の機能を提供するパターンマッチング構文がサポートされました。

そのため、.when.whenOrNullではなく、Dart言語の言語仕様であるパターンマッチング構文の使用が推奨されています。

import 'package:dart_union/dart_freezed_union.dart';

void main(List<String> arguments) {
  final payments = <Payment>[
    const Payment.credit(data: Credit('123-4567-890')),
    const Payment.payPal(data: PayPal('xyz@example.com')),
    const Payment.bank(data: Bank('xxxx-yyyy-zzzz')),
  ];

  for (final payment in payments) {
    payment.when(
      credit: (data) => print('Credit: ${data.cardNumber}'),
      payPal: (data) => print('PayPal: ${data.email}'),
      bank: (data) => print('Bank: ${data.accountNumber}'),
    );
  }
}
Shinya

フリーランス。プログラマー歴10年以上。Amazoned Programmer (Amazonの企業文化に感銘を受けたプログラマー)。

仕事ではJavaとSQLとかを主に使ってアプリケーションを開発しています。BIツールを使用したビッグデータの可視化や解析から、AWSなどのクラウド技術の活用に興味があり、日々研究と研鑽を重ねています。このブログでは日々の学習のアウトプットを投稿していこうと思います。

暇な時にDart/Flutterのパッケージ開発やアプリ開発なども行なっており、OSSのatproto.dartを開発してたりします。

Shinyaをフォローする
DartFlutterProgrammingTech
Shinyaをフォローする
タイトルとURLをコピーしました