Dart 3.3のextension typeとJavaScript APIの連携
Dart 3.3でextension typeが入り、JavaScript APIとの連携が格段によくなったので宣伝します。
Dart 3.2まででは、JSの型情報をDartで表現するには、次のような記述になっていました。
(これでもjsライブラリとjs_interopを比較すると、全然違うのですが、ここでは割愛します)
typedef JSAny = js_types.JSAny;
typedef JSPromise = js_types.JSPromise;
typedef JSArray = js_types.JSArray;
Dart 3.3からの記述は、次のとおりです。
extension type JSAny._(JSAnyRepType _jsAny) implements Object {}
('Promise')
extension type JSPromise<T extends JSAny?>._(JSPromiseRepType _jsPromise)
implements JSObject {
external JSPromise(JSFunction executor);
}
('Array')
extension type JSArray<T extends JSAny?>._(JSArrayRepType _jsArray)
implements JSObject {
external JSArray();
external JSArray.withLength(int length);
}
一見するとそんなに違いがないのですが、Dart 3.3からはジェネリクスが活用できるようになります。
一例として、自作しているtaroライブラリでブラウザのCache APIにアクセスしている処理を抜粋します。
Dart 3.2の時には、次のような定義となり
/// see [https://developer.mozilla.org/en-US/docs/Web/API/Cache]
('Cache')
class Cache {}
extension CacheExtension on Cache {
external JSPromise match(
JSString request,
);
external JSPromise put(
JSString request,
Response response,
);
external JSPromise delete(
JSString request,
);
}
呼び出しを行う際に、as
による型変換が必要です。
final cacheFileJs = await cache.match(cacheFileName).toDart as Response?;
これが、Dart 3.3からは次のような定義になります。
/// [https://developer.mozilla.org/en-US/docs/Web/API/Cache]
extension type Cache(JSObject _) implements JSObject {
external JSPromise<Response?> match(
JSString request,
);
external JSPromise put(
JSString request,
Response response,
);
external JSPromise delete(
JSString request,
);
}
よって、型情報がすでに付与されているので、match
の処理を行う際には
final cacheFileJs = await cache.match(cacheFileName).toDart;
だけでよくなります。
最も便利さを感じるのは、JSArray
です。
例えば、Dart 3.2で次のようなインスタンスを作りたい場合は、ランタイムに任せる必要があります。
class SampleOption {
external factory SampleOption({
JSArray? stringOptions,
JSArray? objectOptions,
});
}
class ObjectOption {
external factory ObjectOption({
JSString? option1,
JSBoolean? option2,
});
}
これが、Dart 3.3ではジェネリクスにより、クラス定義時に型を絞ることができます。
extension type SampleOption._(JSObject _) implements JSObject {
external factory SampleOption({
JSArray<JSString>? stringOptions,
JSArray<ObjectOption>? objectOptions,
});
}
extension type ObjectOption._(JSObject _) implements JSObject {
external factory ObjectOption({
JSString? option1,
JSBoolean? option2,
});
}
Dart 3.3を使いましょう。
Primitive型については、external
なFunctionの引数において、都度JSの型に変換しなくても良いらしい。
なので、次のように書くのがよりシンプル。
/// [https://developer.mozilla.org/en-US/docs/Web/API/Cache]
extension type Cache(JSObject _) implements JSObject {
external JSPromise<Response?> match(
String request,
);
external JSPromise put(
String request,
Response response,
);
external JSPromise delete(
String request,
);
}
もちろん、戻り値やJSPromise
のジェネリクスなどには指定できない。このため、「どれがPrimitiveなのか」を頭に入れた上でレビューをする必要がありそう。
webのコードを眺めていて気付いた。typedef
で型を書いているのも、確かに引数で使うなら便利なのかも。
extension type
で次のように定義をした場合、
extension type GoodObject._(JSObject _) implements JSObject {
external factory GoodObject({
String foo,
String? bar,
});
}
foo
にvalue
を、bar
を指定しない(つまりnull
を指定した)場合には、JSONとして次の形になる様子。
{
"foo": "value",
"bar": null
}
このため、null
の場合にkeyごと削除したい場合には、(現時点では)extension type
による変換が不適切な様子。パッと思いつくあたりだと、一度MapにしてからJSON化すると対応できる。
JSAny convert() {
final map =<String, Object>{};
if (foo != null) {
map['foo'] = foo;
}
if (bar != null) {
map['bar`] = bar;
}
return map.jstify()!;
}
ただ、都度これやるの面倒だし、@JS
のオプションとして提供されないかなーと思っている。