メインコンテンツまでスキップ

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

· 約6分
Shinya Kato
DayoneLabs管理人、ソフトウェア開発者、OSS開発者

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つの型として扱うことができます。

TypeScript
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クラスを継承したサブクラスは同一ファイル内でのみ定義でき、パターンマッチングで網羅性チェックが行われます。

実装

payment.dart
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クラスの場合、すべてのサブクラスを網羅していないとコンパイルエラーになるため、処理漏れを防ぐことができます。

main.dart
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に以下の依存パッケージを追加します。

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

実装

payment.dart
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式によるパターンマッチングが推奨されます。

main.dart
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パッケージが選択肢となります。


参考リンク:

免責事項:
当記事は管理人の開発時に書き留められたメモをもとにAIを活用して編纂されたものです。 管理人は記事の公開前に内容の校正・校閲を行い、記事の信頼性と正確性の向上に務めますが、それらを保証するものではありません。 また、当記事の編集時の誤字やコード例の不具合等によって読者が何らかの損害等を被った場合でも、管理人は一切の責任を負いません。 当記事に掲載したコンテンツの利用については自己責任でお願い致します。