🪢
Dart で無理やり Union 型やろうぜ
Union 型とは
たとえば、number の中でも、特定の値だけに制限したいとか、複数の型を受け入れたい時につけるやつです。
type ErrorCode =
  | 400
  | 401
  | 402
  | 403
  | 404
  | 405;
const codeA: ErrorCode = 400;         // not error
const codeB: ErrorCode = 50000000000; // error
type QueryParameterValue = string | number | boolean;
const valueA: ErrorCode = 'name';         // not error
const valueB: ErrorCode = ['name'];       // error
Dart でやりたいぜ
たとえば、firebase_analytics にこんなコードがあります。
void _assertParameterTypesAreCorrect(Map<String, Object?>? parameters) =>
    parameters?.forEach((key, value) {
      assert(
        value is String || value is num,
        "'string' OR 'number' must be set as the value of the parameter: $key. $value found instead",
      );
    });
これは、Firebase Analytics のパラメータの値を制限するため、想定してない型が来たら assert エラーを出すようにする実装です。
Firebase Analytics のパラメータ使う部分全てで実行されるようになってます。
ですが、これは実行エラーなので、パラメータに String と num 以外を渡しても静的エラーが出ません。
Dart には Union 型がないく、String | num みたいなことができないので、こういう対応をしています。
sealed class 使おうぜ
そこで登場するのが sealed のクラス。
これで、String か num しか受け取らない値を作ることができます。
sealed class FirebaseAnalyticsParameterValue {
  const FirebaseAnalyticsParameterValue._(this.value);
  
  // コンストラクタも用意すると親切
  const factory FirebaseAnalyticsParameterValue.string(String value) =
      FirebaseAnalyticsParameterValueString;
  const factory FirebaseAnalyticsParameterValue.number(num value) =
      FirebaseAnalyticsParameterValueNum;
  final dynamic value;
}
class FirebaseAnalyticsParameterValueString
    extends FirebaseAnalyticsParameterValue {
  const FirebaseAnalyticsParameterValueString(String super.value) : super._();
  
  String get value => super.value;
}
class FirebaseAnalyticsParameterValueNum
    extends FirebaseAnalyticsParameterValue {
  const FirebaseAnalyticsParameterValueNum(num super.value) : super._();
  
  num get value => super.value;
}
実行例
typedef FirebaseAnalyticsParameters = Map<String, FirebaseAnalyticsParameterValue>;
class FirebaseAnalyticsRepository {
  final _analytics =  FirebaseAnalytics.instance;
  Future<void> logEvent({
    required String name,
    FirebaseAnalyticsParameters? parameters,
  }) async {
    await _analytics.logEvent(
      name: name,
      parameters: parameters?.map(
        (key, value) => MapEntry(key, value.value),
      )
    );
  }
}
void main() async {
  const repository = FirebaseAnalyticsRepository();
  
  await repository.logEvent(
    name: 'hoge_event',
    parameters: {
      'param_1': FirebaseAnalyticsParameterValue.string('aaa'),
      'param_2': FirebaseAnalyticsParameterValue.number(111),
    }
  );
}
課題
- 実装がめんどい。
 - 結局 class を経由するので、リテラルそのまま使える Union 型よりは取り回しが効かない。
 
さっき思いついたものを書いて、あんまり試してないので、改善点とか大歓迎です。
Discussion