🪢

Dart で Union 型やろうぜ(実験的に)

2023/12/19に公開

Union 型とは

https://typescriptbook.jp/reference/values-types-variables/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 のクラス。

https://dart.dev/language/class-modifiers#sealed

これで、Stringnum しか受け取らない値を作ることができます。

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