Open3

Dart 3.3のextension typeとJavaScript APIの連携

koji-1009koji-1009

Dart 3.3でextension typeが入り、JavaScript APIとの連携が格段によくなったので宣伝します。

https://dart.dev/interop/js-interop/usage


Dart 3.2まででは、JSの型情報をDartで表現するには、次のような記述になっていました。
(これでもjsライブラリとjs_interopを比較すると、全然違うのですが、ここでは割愛します)

https://github.com/dart-lang/sdk/blob/3.2.0/sdk/lib/js_interop/js_interop.dart

typedef JSAny = js_types.JSAny;

typedef JSPromise = js_types.JSPromise;

typedef JSArray = js_types.JSArray;

Dart 3.3からの記述は、次のとおりです。

https://github.com/dart-lang/sdk/blob/3.3.0/sdk/lib/js_interop/js_interop.dart

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にアクセスしている処理を抜粋します。

https://github.com/koji-1009/taro/blob/0.1.0/lib/src/storage/web_cache.dart

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://github.com/koji-1009/taro/blob/0.2.1/lib/src/storage/web_cache.dart

/// [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を使いましょう。

koji-1009koji-1009

Primitive型については、externalなFunctionの引数において、都度JSの型に変換しなくても良いらしい。

https://dart.dev/interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs

なので、次のように書くのがよりシンプル。

/// [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で型を書いているのも、確かに引数で使うなら便利なのかも。
https://github.com/dart-lang/web/blob/v0.5.0/lib/src/dom/webcryptoapi.dart

koji-1009koji-1009

extension typeで次のように定義をした場合、

extension type GoodObject._(JSObject _) implements JSObject {
  external factory GoodObject({
    String foo,
    String? bar,
  });
}

foovalueを、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のオプションとして提供されないかなーと思っている。