Dart言語で擬似的なUnion型を実装する - sealedクラスとfreezedパッケージ

どうも、Shinyaです。この記事では、Dart言語でTypeScriptのようなUnion型を擬似的に実装する方法について解説します。Dart 3.0で導入されたsealedクラスと、freezedパッケージを使用した2つのアプローチを紹介します。
- DartでUnion型のようなデータ構造を実装したい人
- TypeScriptからDartに移行する際に型の表現方法を知りたい人
- sealedクラスとパターンマッチングについて学びたい人
- freezedパッケージの使い方を知りたい人
DartにはUnion型がない
TypeScriptでは、Union型を使用して複数の型を1つの型として扱うことができます。
type Payment =
| { kind: "credit"; cardNumber: string }
| { kind: "paypal"; email: string }
| { kind: "bank"; accountNumber: string };
function processPayment(payment: Payment): string {
switch (payment.kind) {
case "credit":
return `Credit: ${payment.cardNumber}`;
case "paypal":
return `PayPal: ${payment.email}`;
case "bank":
return `Bank: ${payment.accountNumber}`;
}
}
一方、Dartには言語仕様としてUnion型が存在しません。この機能についてはdart-lang/languageリポジトリで議論が行われていますが、現時点では正式な仕様として採用されていません。
しかし、Dart 3.0以降のsealedクラスやfreezedパッケージを使用することで、Union型に相当するデータ構造を実装できます。
事前準備
この記事の手順を実行するには、Dart SDKが必要です。Dart/Flutterの導入がまだの場合は、HomebrewでFlutter/Dartをインストールするの記事を参照できます。
方法1: sealedクラスを使用する
Dart 3.0で導入されたsealedクラスは、Union型を表現するための言語組み込みの仕組みです。sealedクラスを継承したサブクラスは同一ファイル内でのみ定義でき、パターンマッチングで網羅性チェックが行われます。
実装
sealed class Payment {
const Payment();
}
class Credit extends Payment {
final String cardNumber;
const Credit({required this.cardNumber});
}
class PayPal extends Payment {
final String email;
const PayPal({required this.email});
}
class Bank extends Payment {
final String accountNumber;
const Bank({required this.accountNumber});
}
パターンマッチングで分岐する
Dart 3.0以降のswitch式を使用して、各サブクラスに対する処理を記述できます。sealedクラスの場合、すべてのサブクラスを網羅していないとコンパイルエラーになるため、処理漏れを防ぐことができます。
String processPayment(Payment payment) {
return switch (payment) {
Credit(:final cardNumber) => 'クレジットカード: $cardNumber',
PayPal(:final email) => 'PayPal: $email',
Bank(:final accountNumber) => '銀行口座: $accountNumber',
};
}
void main() {
final payments = [
Credit(cardNumber: '0000-0000-0000-0000'),
PayPal(email: 'user@example.com'),
Bank(accountNumber: '1234567890'),
];
for (final payment in payments) {
print(processPayment(payment));
}
}
実行結果
クレジットカード: 0000-0000-0000-0000
PayPal: user@example.com
銀行口座: 1234567890
sealedクラスは言語組み込みの機能であるため、外部パッケージへの依存が不要で、コード生成も必要ありません。
方法2: freezedパッケージを使用する
freezedは、イミュータブルなデータクラスとUnion型を生成するコードジェネレーターです。ボイラープレートコードを削減し、copyWithや==演算子なども自動生成されます。
依存パッケージの追加
pubspec.yamlに以下の依存パッケージを追加します。
dependencies:
freezed_annotation: ^3.0.0
dev_dependencies:
freezed: ^3.0.0
build_runner: ^2.4.0
または、以下のコマンドで追加できます。
dart pub add freezed_annotation
dart pub add freezed --dev
dart pub add build_runner --dev
実装
import 'package:freezed_annotation/freezed_annotation.dart';
part 'payment.freezed.dart';
sealed class Payment with _$Payment {
const factory Payment.credit({required String cardNumber}) = Credit;
const factory Payment.paypal({required String email}) = PayPal;
const factory Payment.bank({required String accountNumber}) = Bank;
}
コードの生成
以下のコマンドでコードを生成します。
dart run build_runner build --delete-conflicting-outputs
コマンドを実行すると、payment.freezed.dartが自動生成されます。
使用例
freezedで生成されたクラスは.when()メソッドを提供しますが、Dart 3.0以降ではswitch式によるパターンマッチングが推奨されます。
import 'payment.dart';
void main() {
final payments = [
Payment.credit(cardNumber: '0000-0000-0000-0000'),
Payment.paypal(email: 'user@example.com'),
Payment.bank(accountNumber: '1234567890'),
];
for (final payment in payments) {
// Dart 3.0以降のパターンマッチング(推奨)
final message = switch (payment) {
Credit(:final cardNumber) => 'クレジットカード: $cardNumber',
PayPal(:final email) => 'PayPal: $email',
Bank(:final accountNumber) => '銀行口座: $accountNumber',
};
print(message);
// .when() による分岐(従来の方法)
payment.when(
credit: (cardNumber) => print('Credit: $cardNumber'),
paypal: (email) => print('PayPal: $email'),
bank: (accountNumber) => print('Bank: $accountNumber'),
);
}
}
実行結果
クレジットカード: 0000-0000-0000-0000
Credit: 0000-0000-0000-0000
PayPal: user@example.com
PayPal: user@example.com
銀行口座: 1234567890
Bank: 1234567890
sealedクラスとfreezedの比較
| 観点 | sealedクラス | freezed |
|---|---|---|
| 外部依存 | なし | freezed, build_runner が必要 |
| コード生成 | 不要 | 必要(build_runnerを使用) |
| ボイラープレート | 手動で記述 | 自動生成(copyWith, ==, toString) |
| パターンマッチング | switch式で利用可能 | switch式と.when()の両方で利用可能 |
| 導入の手軽さ | 手軽 | 初期設定が必要 |
プロジェクトの規模やデータクラスの数に応じて選択することになりますが、Union型のみが目的であればsealedクラスで十分です。copyWithや==の自動生成が必要な場合はfreezedの使用が適しています。
まとめ
この記事では、Dart言語でUnion型を擬似的に実装する方法として、sealedクラスとfreezedパッケージの2つのアプローチを解説しました。
Dart 3.0以降ではsealedクラスとswitch式のパターンマッチングにより、外部パッケージなしでUnion型に相当するデータ構造を表現できます。より多くのユーティリティが必要な場合はfreezedパッケージが選択肢となります。
参考リンク:
