524 lines
19 KiB
Markdown
524 lines
19 KiB
Markdown
|
|
[](https://pub.dev/packages/js)
|
|||
|
|
[](https://pub.dev/packages/js/publisher)
|
|||
|
|
|
|||
|
|
> [!CAUTION]
|
|||
|
|
> This package is discontinued. Prefer using
|
|||
|
|
> [`dart:js_interop`](https://api.dart.dev/dart-js_interop/dart-js_interop-library.html)
|
|||
|
|
> for JS interop. See the
|
|||
|
|
> [JS interop documentation](https://dart.dev/interop/js-interop) for more
|
|||
|
|
> details.
|
|||
|
|
|
|||
|
|
Use this package when you want to call JavaScript APIs from Dart code, or vice
|
|||
|
|
versa.
|
|||
|
|
|
|||
|
|
This package's main library, `js`, provides annotations and functions that let
|
|||
|
|
you specify how your Dart code interoperates with JavaScript code. The
|
|||
|
|
Dart-to-JavaScript compilers — dartdevc and dart2js — recognize these
|
|||
|
|
annotations, using them to connect your Dart code with JavaScript.
|
|||
|
|
|
|||
|
|
A second library in this package, `js_util`, provides low-level utilities that
|
|||
|
|
you can use when it isn't possible to wrap JavaScript with a static, annotated
|
|||
|
|
API.
|
|||
|
|
|
|||
|
|
[JS interop documentation]: https://dart.dev/interop/js-interop
|
|||
|
|
|
|||
|
|
## Static Interop
|
|||
|
|
|
|||
|
|
**Important:** Static interop is now supported with extension types in Dart 3.3.
|
|||
|
|
Prefer using `dart:js_interop` with extension types instead of `@staticInterop`.
|
|||
|
|
See the [JS interop documentation] for more details.
|
|||
|
|
|
|||
|
|
In the past, `package:js` has allowed users to use JavaScript interoperability
|
|||
|
|
in more dynamic, class-based ways. While we will continue to allow users to use
|
|||
|
|
that functionality in the foreseeable future, Dart is transitioning to a more
|
|||
|
|
static, inline class-based interop. What this largely means is that we're moving
|
|||
|
|
away from dynamic invocations and instead requiring static typing to use
|
|||
|
|
interop. We're calling this model "static interop".
|
|||
|
|
|
|||
|
|
We are doing this for several reasons, such as idiomaticity, performance, type
|
|||
|
|
soundness, ability to interop with DOM types, and compatibility with Wasm. This
|
|||
|
|
is an ongoing effort that will affect our web library offerings as well.
|
|||
|
|
|
|||
|
|
In version 0.6.6, we introduced a way to use static interop with
|
|||
|
|
`@staticInterop`. `package:js` classes that have this annotation are required
|
|||
|
|
to use the new static semantics via extensions and do not support dynamic
|
|||
|
|
invocations. For more details on how to use `@staticInterop` classes, see below.
|
|||
|
|
To test these classes, see the sections below on `@JSExport` and
|
|||
|
|
`js_util.createStaticInteropMock`.
|
|||
|
|
|
|||
|
|
As this is an ongoing effort, we are also working on interop with inline
|
|||
|
|
classes. [Inline classes][] will be a new language feature that enables
|
|||
|
|
zero-cost wrapping. In the future, users should opt-in to the semantics of
|
|||
|
|
static interop using inline classes instead of `@staticInterop`. It will be
|
|||
|
|
easier to use, more idiomatic, and better supported going forward.
|
|||
|
|
|
|||
|
|
For now, static interop remains experimental and in development. We may make
|
|||
|
|
breaking changes in the future. We'll update this text when the new interop
|
|||
|
|
model matures and is considered stable.
|
|||
|
|
|
|||
|
|
[Inline classes]: https://github.com/dart-lang/language/issues/2727
|
|||
|
|
|
|||
|
|
## Usage
|
|||
|
|
|
|||
|
|
The following examples show how to handle common interoperability tasks.
|
|||
|
|
|
|||
|
|
### Calling JavaScript functions
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS()
|
|||
|
|
library stringify;
|
|||
|
|
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
// Calls invoke JavaScript `JSON.stringify(obj)`.
|
|||
|
|
@JS('JSON.stringify')
|
|||
|
|
external String stringify(Object obj);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Using JavaScript namespaces and classes
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS('google.maps')
|
|||
|
|
library maps;
|
|||
|
|
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
// Invokes the JavaScript getter `google.maps.map`.
|
|||
|
|
external Map get map;
|
|||
|
|
|
|||
|
|
// The `Map` constructor invokes JavaScript `new google.maps.Map(location)`
|
|||
|
|
@JS()
|
|||
|
|
class Map {
|
|||
|
|
external Map(Location location);
|
|||
|
|
external Location getLocation();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The `Location` constructor invokes JavaScript `new google.maps.LatLng(...)`
|
|||
|
|
//
|
|||
|
|
// We recommend against using custom JavaScript names whenever
|
|||
|
|
// possible. It is easier for users if the JavaScript names and Dart names
|
|||
|
|
// are consistent.
|
|||
|
|
@JS('LatLng')
|
|||
|
|
class Location {
|
|||
|
|
external Location(num lat, num lng);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Passing object literals to JavaScript
|
|||
|
|
|
|||
|
|
Many JavaScript APIs take an object literal as an argument. For example:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
// JavaScript
|
|||
|
|
printOptions({ responsive: true });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
If you want to use `printOptions` from Dart a `Map<String, dynamic>` would be
|
|||
|
|
"opaque" in JavaScript.
|
|||
|
|
|
|||
|
|
Instead, create a Dart class with both the `@JS()` and `@anonymous` annotations.
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS()
|
|||
|
|
library print_options;
|
|||
|
|
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
void main() {
|
|||
|
|
printOptions(Options(responsive: true));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@JS()
|
|||
|
|
external printOptions(Options options);
|
|||
|
|
|
|||
|
|
@JS()
|
|||
|
|
@anonymous
|
|||
|
|
class Options {
|
|||
|
|
external bool get responsive;
|
|||
|
|
|
|||
|
|
// Must have an unnamed factory constructor with named arguments.
|
|||
|
|
external factory Options({bool responsive});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Making a Dart function callable from JavaScript
|
|||
|
|
|
|||
|
|
If you pass a Dart function to a JavaScript API as an argument, wrap the Dart
|
|||
|
|
function using `allowInterop()` or `allowInteropCaptureThis()`.
|
|||
|
|
|
|||
|
|
To make a Dart function callable from JavaScript _by name_, use a setter
|
|||
|
|
annotated with `@JS()`.
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS()
|
|||
|
|
library callable_function;
|
|||
|
|
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
/// Allows assigning a function to be callable from `window.functionName()`
|
|||
|
|
@JS('functionName')
|
|||
|
|
external set _functionName(void Function() f);
|
|||
|
|
|
|||
|
|
/// Allows calling the assigned function from Dart as well.
|
|||
|
|
@JS()
|
|||
|
|
external void functionName();
|
|||
|
|
|
|||
|
|
void _someDartFunction() {
|
|||
|
|
print('Hello from Dart!');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void main() {
|
|||
|
|
_functionName = allowInterop(_someDartFunction);
|
|||
|
|
// JavaScript code may now call `functionName()` or `window.functionName()`.
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### @staticInterop
|
|||
|
|
|
|||
|
|
With `package:js`, we have historically had two different types of classes:
|
|||
|
|
plain `@JS` (those with just the `@JS` annotation) and `@anonymous` classes.
|
|||
|
|
Now, you can use a new one: `@staticInterop`.
|
|||
|
|
|
|||
|
|
These classes are different in that they do not allow instance members within
|
|||
|
|
the class itself. All such members need to go into an extension (hence
|
|||
|
|
“static”). Let’s look at an example:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS()
|
|||
|
|
library static_interop;
|
|||
|
|
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
// Assumes there is a top-level `StaticInterop` class in a JS module.
|
|||
|
|
@JS()
|
|||
|
|
@staticInterop
|
|||
|
|
class StaticInterop {
|
|||
|
|
external factory StaticInterop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension on StaticInterop {
|
|||
|
|
external int field;
|
|||
|
|
external int get getSet;
|
|||
|
|
external set getSet(int val);
|
|||
|
|
external int method();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void main() {
|
|||
|
|
var jsObj = StaticInterop();
|
|||
|
|
jsObj.field = 1;
|
|||
|
|
jsObj.method();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The `external` static extension members get lowered to JS naturally:
|
|||
|
|
`jsObj.field` becomes a property get of `field` in JS and `jsObj.method()`
|
|||
|
|
becomes a function invocation of `method` on `jsObj`.
|
|||
|
|
|
|||
|
|
In many ways, these classes are just like the plain `@JS` and `@anonymous`
|
|||
|
|
classes. Like with plain `@JS` classes, you can provide a value in `@JS` if you
|
|||
|
|
want the constructor to use a particular JS class e.g.
|
|||
|
|
`@JS(‘module.MyJSClass’)`. You can also add `@anonymous` to `@staticInterop`
|
|||
|
|
classes if you want the factory constructor with named arguments in order to
|
|||
|
|
make an object literal e.g.
|
|||
|
|
`external factory AnonymousStaticInterop({int? field1, int? field2})`. Also like
|
|||
|
|
with plain `@JS` classes, you can’t inherit non-`package:js` classes. You should
|
|||
|
|
only inherit other `@staticInterop` classes for subtyping and inheriting
|
|||
|
|
extension methods. Lastly, you can freely cast JS objects to and from the three
|
|||
|
|
types of `package:js` classes.
|
|||
|
|
|
|||
|
|
What makes `@staticInterop` unique, however, is that you can use them to
|
|||
|
|
represent DOM objects as well as other JS objects, which you can’t with previous
|
|||
|
|
`package:js` classes. Historically, you’ve needed to use `dart:html` to interact
|
|||
|
|
with the DOM e.g. `DivElement`. Now, you can create your own abstraction for
|
|||
|
|
these objects instead of using the ones we provide in `dart:html`:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS()
|
|||
|
|
library static_interop;
|
|||
|
|
|
|||
|
|
import 'dart:html' as html;
|
|||
|
|
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
@JS()
|
|||
|
|
@staticInterop
|
|||
|
|
class JSWindow {}
|
|||
|
|
|
|||
|
|
extension JSWindowExtension on JSWindow {
|
|||
|
|
external String get name;
|
|||
|
|
String get nameAllCaps => name.toUpperCase();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void main() {
|
|||
|
|
var jsWindow = html.window as JSWindow;
|
|||
|
|
print(jsWindow.name.toUpperCase() == jsWindow.nameAllCaps);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Note that you can have both `external` and non-`external` members in the
|
|||
|
|
extension.
|
|||
|
|
|
|||
|
|
Compared to non-`@staticInterop` `package:js` classes, `@staticInterop` classes:
|
|||
|
|
|
|||
|
|
- Are more performant
|
|||
|
|
- Have better type guarantees
|
|||
|
|
- Generate less code
|
|||
|
|
- Allow non-`external` members
|
|||
|
|
- Allow `external` extension members to be renamed using `@JS()` e.g.
|
|||
|
|
`@JS('renamedField')`
|
|||
|
|
|
|||
|
|
The only catch is that virtual/dynamic dispatch is _disallowed_. That means
|
|||
|
|
methods are resolved using only the _static_ type of the object.
|
|||
|
|
|
|||
|
|
In general, it's advised to use `@staticInterop` wherever you can, as future JS
|
|||
|
|
interop will only target static dispatch.
|
|||
|
|
|
|||
|
|
### @JSExport and js_util.createDartExport
|
|||
|
|
|
|||
|
|
One of the difficulties with JS interop is that most of it is exclusively
|
|||
|
|
focused on importing JS code to Dart, not the other way around. We have some
|
|||
|
|
functionality like `allowInterop`, which allows you to call Dart functions in
|
|||
|
|
JS, but this becomes cumbersome when you want to use a Dart object. You need to
|
|||
|
|
essentially `allowInterop` all members manually.
|
|||
|
|
|
|||
|
|
`createDartExport` instead lets you do this automatically. Let’s see how with an
|
|||
|
|
example:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
import 'dart:js_util';
|
|||
|
|
|
|||
|
|
import 'package:expect/expect.dart';
|
|||
|
|
import 'package:js/js.dart';
|
|||
|
|
|
|||
|
|
// The Dart class must have `@JSExport` on it or one of its instance members.
|
|||
|
|
@JSExport()
|
|||
|
|
class Counter {
|
|||
|
|
int value = 0;
|
|||
|
|
@JSExport('increment')
|
|||
|
|
void renamedIncrement() {
|
|||
|
|
value++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@JS()
|
|||
|
|
@staticInterop
|
|||
|
|
class JSCounter {}
|
|||
|
|
|
|||
|
|
extension on JSCounter {
|
|||
|
|
external int value;
|
|||
|
|
external void increment();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void main() {
|
|||
|
|
var dartCounter = Counter();
|
|||
|
|
var counter = createDartExport<Counter>(dartCounter) as JSCounter;
|
|||
|
|
Expect.equals(0, counter.value);
|
|||
|
|
counter.increment();
|
|||
|
|
Expect.equals(1, counter.value);
|
|||
|
|
Expect.equals(1, dartCounter.value); // Dart object gets modified
|
|||
|
|
dartCounter.value = 0;
|
|||
|
|
Expect.equals(0, counter.value); // Changes in Dart object affect the exported object
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
There are a number of things happening here. At a high level, you pass
|
|||
|
|
`createDartExport` an instance of some Dart object that has `@JSExport` either
|
|||
|
|
on it or one of its instance members and the object’s static type if needed.
|
|||
|
|
Using the static type, we transform the `createDartExport` call into a JS object
|
|||
|
|
literal that is a mapping from each member’s Dart name (accounting for renames
|
|||
|
|
using the `@JSExport` annotation) to the member. The JS object essentially wraps
|
|||
|
|
and acts as a proxy to the exported Dart object.
|
|||
|
|
|
|||
|
|
Now, when we use it as a JS object (in this case, using `@staticInterop`), we
|
|||
|
|
can use the same names to access these members. We can also use the same syntax
|
|||
|
|
to access these members e.g. `counter.value = 0`. This now gives us an easy to
|
|||
|
|
do what we wanted before with `allowInterop` for each member.
|
|||
|
|
|
|||
|
|
There are, of course, limitations.
|
|||
|
|
|
|||
|
|
The only members that are “exported” are concrete instance members i.e. fields,
|
|||
|
|
getters, setters, and methods. That means you can’t export static members,
|
|||
|
|
constructors, factories, operators (the syntax complicates things), and
|
|||
|
|
extension methods. You can still have these members - they just won’t be present
|
|||
|
|
in the resulting exported object. Of course, you can use another instance member
|
|||
|
|
to call these members as well, and _that_ instance member will be exported.
|
|||
|
|
|
|||
|
|
In order to use `createDartExport`, you need to have a class that uses
|
|||
|
|
`@JSExport`.If you want to export only some members of a class, omit the
|
|||
|
|
annotation on the class, and only use it on the members you want. If you need to
|
|||
|
|
rename members, you can provide the `@JSExport` annotation on that member a
|
|||
|
|
string value, similar to renaming done via `@JS()`. Inheritance respects the
|
|||
|
|
individual superclass’ annotations. In other words, if the class of the object
|
|||
|
|
you want to export has a superclass, but that superclass has no `@JSExport`
|
|||
|
|
annotation anywhere, none of its superclass’ members are exported.
|
|||
|
|
|
|||
|
|
Lastly, different members can’t have the same export name, unless they are a
|
|||
|
|
getter and setter pair. So, for example, if you have a field and a method and
|
|||
|
|
one of them is renamed to the other’s name, that’s a conflict:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JSExport()
|
|||
|
|
class DartClass {
|
|||
|
|
int member = 0;
|
|||
|
|
@JSExport('member') // Two incompatible members have the same export name.
|
|||
|
|
void method() {}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This holds true with inheritance as well, unless the member is overridden.
|
|||
|
|
|
|||
|
|
### js_util.createStaticInteropMock
|
|||
|
|
|
|||
|
|
One of the neat things about the above example with `Counter` is we’ve
|
|||
|
|
essentially created a mock for `JSCounter`. In the past, to mock a plain `@JS`
|
|||
|
|
or `@anonymous` class, you could create a Dart class that `implements` that
|
|||
|
|
interop class, and due to Dart's virtual dispatch, this would call the Dart
|
|||
|
|
class' members instead. Now that we're using `external` extension members, this
|
|||
|
|
no longer works. We now have to mock at the _JS level_ instead. With
|
|||
|
|
`createDartExport`, you’re essentially using a Dart object to replace a JS
|
|||
|
|
object. This functionality is equivalent to mocking at the JS level, and you can
|
|||
|
|
also use it to mock the old non-`@staticInterop` `package:js` classes!
|
|||
|
|
|
|||
|
|
One useful feature of the old style of mocking using `implements` is it lets you
|
|||
|
|
know if you've implemented the needed members. We can't do that with
|
|||
|
|
`createDartExport`. For example:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JSExport()
|
|||
|
|
class Counter {
|
|||
|
|
// Where is `value` and `increment`?
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This would obviously not be a satisfactory mock for `JSCounter`.
|
|||
|
|
`createDartExport` has no idea what class you're trying to mock, so it can't
|
|||
|
|
tell you if you’ve got your mock class right.
|
|||
|
|
|
|||
|
|
This is where `createStaticInteropMock` comes in. It takes in a separate type
|
|||
|
|
argument, e.g. `createStaticInteropMock<JSCounter, Counter>(Counter())`, to
|
|||
|
|
determine whether mocking _conformance_ is satisfied. This type argument must be
|
|||
|
|
a `@staticInterop` class. With this, you’ll see an error saying that you haven’t
|
|||
|
|
implemented all the needed members. If the mock class implements all the needed
|
|||
|
|
members, the function does the same thing as `createDartExport`, and returns an
|
|||
|
|
object literal that wraps the Dart object.
|
|||
|
|
|
|||
|
|
You can also use `package:mockito` to do the mocking with this API, by providing
|
|||
|
|
a generated mocking object from `package:mockito` to `createStaticInteropMock`.
|
|||
|
|
|
|||
|
|
There are some corner cases here that are worth noting.
|
|||
|
|
|
|||
|
|
It is possible, through the expressiveness of extension methods, to have name
|
|||
|
|
conflicts like this:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
@JS()
|
|||
|
|
@staticInterop
|
|||
|
|
class StaticInterop {}
|
|||
|
|
|
|||
|
|
extension A on StaticInterop {
|
|||
|
|
external Function member;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension B on StaticInterop {
|
|||
|
|
external void member();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This present an issue as a single Dart class cannot implement `member` as both a
|
|||
|
|
field and a function. So, what to do? We require that you only implement _one_
|
|||
|
|
of these members. So, either a Function field or a function are satisfactory.
|
|||
|
|
|
|||
|
|
It is also sometimes desired that the mocking object is the same underlying type
|
|||
|
|
as the JS object you are interfacing. For example, if you want to mock a JS
|
|||
|
|
`Element`, you’d want the type of the mocking object to also be a `Element` in
|
|||
|
|
order to pass `instanceof` checks. In order to do this, we let users pass the JS
|
|||
|
|
prototype of the type they want the mocking object to be as an argument to
|
|||
|
|
`createStaticInteropMock`.
|
|||
|
|
|
|||
|
|
An important note here is that `createStaticInteropMock` looks for _all_
|
|||
|
|
extensions of the `@staticInterop` type in the program, even if they are out of
|
|||
|
|
scope of the current file. In order to avoid a case where other libraries
|
|||
|
|
extending the `@staticInterop` type break your usage of
|
|||
|
|
`createStaticInteropMock`, you should try to only use this API in tests.
|
|||
|
|
`createStaticInteropMock` is meant to detect issues earlier at compile-time, but
|
|||
|
|
if it's too restrictive, you can still use `createDartExport` to workaround that
|
|||
|
|
(and please provide us feedback on why it's restrictive!).
|
|||
|
|
|
|||
|
|
## Reporting issues
|
|||
|
|
|
|||
|
|
Please file bugs and feature requests on the [SDK issue tracker][issues].
|
|||
|
|
|
|||
|
|
[issues]: https://goo.gl/j3rzs0
|
|||
|
|
|
|||
|
|
## Known limitations and bugs
|
|||
|
|
|
|||
|
|
<!-- [TODO: add intro. perhaps move this to another page?] -->
|
|||
|
|
|
|||
|
|
### Differences between dart2js and dartdevc
|
|||
|
|
|
|||
|
|
Dart's production and development JavaScript compilers use different calling
|
|||
|
|
conventions and type representation, and therefore have different challenges in
|
|||
|
|
JavaScript interop. There are currently some known differences in behavior and
|
|||
|
|
bugs in one or both compilers.
|
|||
|
|
|
|||
|
|
#### Dartdevc and dart2js have different representation for Maps
|
|||
|
|
|
|||
|
|
Passing a `Map<String, String>` as an argument to a JavaScript function will
|
|||
|
|
have different behavior depending on the compiler. Calling something like
|
|||
|
|
`JSON.stringify()` will give different results.
|
|||
|
|
|
|||
|
|
**Workaround:** Only pass object literals instead of Maps as arguments. For json
|
|||
|
|
specifically use `jsonEncode` in Dart rather than a JS alternative.
|
|||
|
|
|
|||
|
|
#### Missing validation for anonymous factory constructors in dartdevc
|
|||
|
|
|
|||
|
|
When using an `@anonymous` class to create JavaScript object literals dart2js
|
|||
|
|
will enforce that only named arguments are used, while dartdevc will allow
|
|||
|
|
positional arguments but may generate incorrect code.
|
|||
|
|
|
|||
|
|
**Workaround:** Try builds in both development and release mode to get the full
|
|||
|
|
scope of static validation.
|
|||
|
|
|
|||
|
|
### Common problems
|
|||
|
|
|
|||
|
|
Dart and JavaScript have different semantics and common patterns, which makes it
|
|||
|
|
easy to make some mistakes and difficult for the tools to provide safety. These
|
|||
|
|
common problems are also known as _sharp edges_.
|
|||
|
|
|
|||
|
|
#### Lack of runtime type checking
|
|||
|
|
|
|||
|
|
The return types of methods annotated with `@JS()` are not validated at runtime,
|
|||
|
|
so an incorrect type may "leak" into other Dart code and violate type system
|
|||
|
|
guarantees. This is not true for `@staticInterop` classes unless the
|
|||
|
|
`@trustTypes` annotation is used.
|
|||
|
|
|
|||
|
|
**Workaround:** For any calls into JavaScript code that are not known to be safe
|
|||
|
|
in their return values, validate the results manually with `is` checks.
|
|||
|
|
|
|||
|
|
#### List instances coming from JavaScript will always be `List<dynamic>`
|
|||
|
|
|
|||
|
|
A JavaScript array does not have a reified element type, so an array returned
|
|||
|
|
from a JavaScript function cannot make guarantees about it's elements without
|
|||
|
|
inspecting each one. At runtime a check like `result is List` may succeed, while
|
|||
|
|
`result is List<String>` will always fail.
|
|||
|
|
|
|||
|
|
**Workaround:** Use `.cast()` or construct a new `List` to get an instance with
|
|||
|
|
the expected reified type. For instance if you want a `List<String>` use
|
|||
|
|
`.cast<String>()` or `List<String>.from`.
|
|||
|
|
|
|||
|
|
#### The `JsObject` type from `dart:js` can't be used with `@JS()` annotation
|
|||
|
|
|
|||
|
|
`JsObject` and related code in `dart:js` uses a different approach and may not
|
|||
|
|
be passed as an argument to a method annotated with `@JS()`.
|
|||
|
|
|
|||
|
|
**Workaround:** Avoid importing `dart:js` and only use the `package:js` provided
|
|||
|
|
approach. To handle object literals use `@anonymous` on an `@JS()` annotated
|
|||
|
|
class.
|
|||
|
|
|
|||
|
|
#### `is` checks and `as` casts between JS interop types will always succeed
|
|||
|
|
|
|||
|
|
For any two `@JS()` types, with or without `@anonymous`, a check of whether an
|
|||
|
|
object of one type `is` another type will always return true, regardless of
|
|||
|
|
whether those two types are in the same prototype chain. Similarly, an explicit
|
|||
|
|
cast using `as` will also succeed.
|