[![pub package](https://img.shields.io/pub/v/js.svg)](https://pub.dev/packages/js) [![package publisher](https://img.shields.io/pub/publisher/js.svg)](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` 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(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(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 ### 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` 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` 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` 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` use `.cast()` or `List.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.