どうも、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言語のインストールが必須となります。そのため、Dart言語をまだインストールしていない方は、以下の記事を参考にDart言語をインストールしてください。
Dart言語で擬似的なUnion型を実装していく
それでは、早速Dart言語でUnion型を実装していきましょう。Dart言語でUnion型を実装する主な方法は次の通りです。
上記の2パターンについて、この記事では参考までに次の構造を持つ簡単なUnion型を実装していきます。
- 支払方法(Payment)
- クレジットカード(Credit)
- カード番号(cardNumber)
- ペイパル(PayPal)
- メールアドレス(email)
- 銀行(Bank)
- 顧客番号(accountNumber)
- クレジットカード(Credit)
sealedクラスを使用してUnion型を実装する
selaedクラスを使用したUnion型の実装のポイントは次の通りです。
- Union型として扱いたいオブジェクトをsealedクラスとして定義(e.g. Payment)
- Union型の要素として扱いたいオブジェクトで上記のsealedクラスをimplements
- 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型の実装のポイントは次の通りです。
- 必須のパッケージをpubspec.yamlの依存性に追加
- freezedを使用してオブジェクトを作成するファイルに次のインポートを追加
- i
mport 'package:freezed_annotation/freezed_annotation.dart';
- i
- freezedを使用してオブジェクトを作成するファイルに次の定義を追加
part '任意のファイル名.freezed.dart';
- 上記の「任意のファイル名」にはfreezedオブジェクトを定義したファイル名と同一の値を指定してください
- Union型の要素として扱いたいオブジェクトを任意の形式で実装
- Union型として扱いたいオブジェクトをfreezedのテンプレートに従って実装
- freezedを使用してコード生成するためコマンドプロンプトで次のコマンドを実行
dart run build_runner build --delete-conflicting-outputs
例えば、次のように実装できます。
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);
}
freezedはbuild_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
といった便利なユーティリティが利用できます。
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}'),
);
}
}