1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Dartのtypedefとextensionの組み合わせが、思ったように動かなかった話

Posted at

tl;dr

https://dartpad.dev/?id=c34c85e9c3a6e873c9f29ff6229bb130 を見て。

やろうと思ったこと

DDD的(といって合っているかわからないけど)に、「ちゃんとデータクラスを作って、そこに表現力を持たせたい」と思いました。

例えば、裏側で持っているIDは16桁ぐらいあるけど、ユーザーには末尾の4桁しか出さない、みたいなことを Id クラスのgetterに押し込めたいと思いました。
以下、この例に対して考えていきます。

Dartでかんたんに実現しようとした

Id クラスとは言っても、基本的には文字列としての機能を持っていてほしく、別クラスとしてちゃんと宣言するのはめんどうに感じてました。

そこで、Dart 2.13 から typedef を使って、エイリアスを作成しようと思いました。
https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067

また、その宣言したエイリアスに対して extension を利用して、機能を追加しようと思いました。

typedef Id = String;

extension on Id {
  String get forUser => substring(length - 4);
}

これ自体は機能するのですが、弊害が大きすぎました。

想定外だった弊害

上記の forUser は、あくまで Id として宣言された文字列だけに適用したいものでした。
(他の文字列では、利用できない方が良い。)

// これはちゃんと動いてほしいし、ちゃんと動く
final Id idVar = '1234567890123456';
print('call from Id ${idVar.forUser}');

// これはコンパイルエラーになってほしい、けど動く
final String stringVar = 'hogehoge';
print('call from String ${stringVar.forUser}');

上のコード内のコメントに書いたとおり、ただの String 型(エイリアス元の型)に対しても、Functionが追加されてしまいました。

これでは、「名前」や「住所」といった文字列に対してでも forUser を実行できてしまい、誤った利用を防げません。
また、コード補完もノイズが増えてしまい、劣化してしまいます。

どうしたか

value として String 型のpropertyを持つクラスを定義して、その中に独自のFunctionを追加しました。

class Id {
  const Id(this.value);
  final String value;

  String get forUser => value.substring(value.length - 4);
}

String 型が持っている多くの機能は Id は持っていない状態となりましたが、必要になったら、必要になったFunctionだけを実装していく形で大丈夫だろう、と判断しました。

参考: https://qiita.com/tokkun5552/items/5dcb79e5283a67c2b2fe#valueobject

補足:Genericsを利用した場合

ちょっと不安になって、 List<String> に追加したfunctionが、 List<int> に追加されないことは確認しました。

typedef IdList = List<Id>;

extension on IdList {
  String get firstItem => this[0];
}

void main() {
  final List<String> stringIdList = <String>['234', '567'];
  // `firstItem` がちゃんと呼び出せる
  print('firstItem ${stringIdList.firstItem}');

  final List<int> intList = <int>[1, 2, 3];
  // ちゃんとコンパイルエラーになる
//   print('firstItem ${intList.firstItem}'); // <- compile error
}

これは、大丈夫そうでした。

補足:他の言語は?

ちゃんと確かめてないですが、他の言語での状況を調べてみました。

Kotlin

Androidアプリ開発でよく利用するKotlinでの状況を調べてみました。

https://discuss.kotlinlang.org/t/extension-functions-on-type-aliases-should-only-be-accessable-from-similar-types/3722/2
https://discuss.kotlinlang.org/t/implicit-conversions-between-type-aliases/3222
こちらの、言語公式コミュニティの方では、 typealias はあくまで"ニックネーム"としてつけるだけで、Dartと同じ動作になりそうでした。

https://kotlinlang.org/docs/inline-classes.html
https://maku77.github.io/kotlin/misc/inline-class.html
ただ、Kotlinには "inline class" というものがあり、それを利用することで実現できそうです。
("inline class"の主眼はパフォーマンス最適化のようですが。)

https://github.com/dart-lang/language/issues/2727
Dart言語でも、inline classの導入は議論されているようでした。(昨年末ぐらいの話っぽい)

Swift

こちらはiOSアプリ開発でよく利用するSwift。

https://qiita.com/mono0926/items/1b94242d4139d1982a31
ほぼこの記事で詳細に説明されてました。
やはり、 typealias はあくまで「別の名前をつけただけ」であって、全く区別されないとのことでした。Dartと同じですね。
Swiftでは、Kotlinの"inline class"のように手軽に実現する手段はなさそうですね。
いくつかの方法が例示されていますが、 ExpressibleByXXXLiteral を利用する方法は、「知らないとできない」ものだと思うので、この機会に知ることができてよかったです。

Go

上記のSwiftのところであげた記事で記載されていましたが、Go言語の type では、Dartと違う動きになりそうでした。

1
2
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?