Skip to content

Improve JS interop #35084

Closed
Closed
@jmesserly

Description

@jmesserly

We'd like to significantly improve Dart's ability to interoperate with JavaScript libraries and vice versa. This will build on existing capabilities (see examples of current JS interop).

The new design will take advantage of Dart 2's sound static typing to provide JS interop that is convenient, high performance, and with minimal code size impact.

Some of the tasks for this:

  • Fully specify JS interop, including all details (Need spec and compile-time errors of incorrect JS interop usage #32929)
  • Fix incompatibilities between dart2js and dartdevc
  • Design it to optimize well in both dart web compilers
  • Implement static analysis (errors/warnings/hints) to help use it correctly, and prevent using unsupported patterns
  • Provide a convenient way of writing interface definitions (see firebase-dart for some examples--it's the best we can do with existing JS interop, but we can do better)
  • Implement tools for automatic interface generation (e.g. from typescript files, web IDL)

Some of the things we'll need to address (details are subject to change):

  • Support for renaming JS APIs (including instance members, see Allow @JS on class method #24779)
  • Dart APIs can be exported to JS (retain them during tree-shaking and ensure they're available from a consistent location)
  • Common Dart & JS types should interoperate well (e.g. Future/Promise, Iterables, Maps, Sets, DateTime, JS Objects used as maps through a JSMap type)
  • When necessary, wrappers and coercions will be implicitly done (i.e. no hand written boilerplate for doing those), possibly guided by annotations in the interface definition (e.g. an annotation to convert a parameter from a Map to a raw JS object)
  • Objects created in JS should be able to bypass Dart's reified generic checks (e.g. Arrays created in JS would not be subject to strict type checks, see @JS() doesn't support or fail on external List<NotDynamic> get #34195 for an example)
  • JS interop types should not be subject to runtime cast failures (similar to the "anonymous" types currently supported)
  • Ability to do JS dynamic dispatch distinct from Dart's dynamic (e.g. JSDynamic that dispatches directly to JS members)
  • Dart classes should be able to extend a JS interop class (e.g. for custom elements), and vice versa if possible (probably opt-in from the Dart class, so it explicitly exports itself for JS subclassing)
  • Ability to rename/hide JS APIs, including ones that start with "_"
  • "extension method" APIs can be added to JS interop classes (note: also includes getters/setters)
  • No metadata needs to be retained for JS types, and no RTTI needs to be computed at runtime

Other details:

Dynamic dispatch support:

We will likely want to keep opt-in dynamic dispatch support, similar to what "dart:html" currently provides, but well specified and available for use by any JS interop class. The classes must opt-in for this. This will cause extra data to be retained for use by runtime dispatch. Type checks are not required, however. The types will use the relaxed JS interop casting rules. Calling conventions in dart:html can likely be simplified (e.g. we do not require obj.foo(x) and obj.foo(x, null) to substitute undefined for null). Similar to JS Array and JS function types, tearoffs of JS methods will be untyped and not checked (they can be converted to any function type).

Because of these changes, we should be able to reduce the signature data stored for dart:html. These changes should be largely non-breaking, as code rarely relies on dart:html throwing type errors (indeed, such code will rarely work in production mode, which omits checks). It is also unlikely that any code is relying on dynamic tearoffs having a specific reified function type.

Note that any data for automatic conversions, or extension methods added to the types, will need to be stored so runtime dispatch can access it. Currently it is implementation defined how these methods are stored (dart2js uses interceptors , dartdevc adds symbolized members to the JS classes). If we want these methods to be accessible from JS (e.g. they are exported), we'll need to specify this precisely. A similar issue exists for the static extension methods (described above).

Longer term: we'd like to use the new JS interop to implement the DOM in a package, and then migrate from "dart:html". This will necessarily be a slow migration/deprecation process, since "dart:html" is necessary for all web apps today. It may involve automated refactoring tools or opt-in static analysis to assist the migration.

Due to these changes, migration to the new dart:html package should not be difficult (likely as simple as importing "package:html/html1.dart"). We will want a static-only version of dart:html, however (e.g. "package:html/html2.dart"). Types from those two HTML libraries should be compatible, so one can cast between them (after all, it is the same JS object underneath). At compile time, an explicit cast may be required (we could allow old types to be converted to new types, though, and you could always go in the other direction with as html1.Element because runtime casting will be allowed).

Ideally, we use re-export to declare the "dynamic version" of package:html:

// in file html1.dart
import "package:js/js.dart" as js;

@js.SupportsDynamicDispatch()
export "html2.dart";

That library simply indicates that dynamic dispatch data should be generated. If "html1.dart" is not imported, the runtime dispatch data would not be generated.

Metadata

Metadata

Labels

P2A bug or feature request we're likely to work onarea-webUse area-web for Dart web related issues, including the DDC and dart2js compilers and JS interop.customer-dart-sasscustomer-google3web-js-interopIssues that impact all js interop

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions