History of JS interop in Dart

Support for Wasm just landed in the current Flutter beta, thanks to an exciting JavaScript interop milestone reached in Dart 3.3. To celebrate, we’re taking a look back at the decade-long journey of Dart and JS interoperability.

Sigmund Cherem
Dart

--

AI Image generated by Gemini

Interoperability has been a core focus from the beginning of Dart. When Dart was first released in 2011, it was designed to be embeddable and multi-platform. It ran on a standalone virtual machine, embedded in a browser, and compiled to JavaScript. When Flutter came along in 2015, we were ready to embed it there, too. Now, we’re excited to target WasmGC runtimes, as well.

At first, we worked quickly to expose the capabilities of each platform where Dart was embedded. That’s how our SDK platform-specific libraries emerged: dart:io exposed the file system on the VM, dart:html exposed the browser APIs on the web, and so on. These libraries looked and felt like regular Dart libraries, but behind the scenes hid some sophisticated low-level, native primitives to make them work. This was the very first form of interop we ever invented. It was expressive, but restricted to only SDK libraries.

On the web, developers needed access to more than just browser APIs. So we started looking at ways to open interoperability to cover more targets. As a starting point, we introduced dart:js in 2013 to enable access to JavaScript libraries.

// Short example JavaScript code to illustrate Dart/JS interop
window.myTopLevel = {
field1: 0,
method2() {
return this.field1;
}
}
// Access via `dart:js` (2013)
import 'dart:js' as js;

void main() {
// This line has a typo! oops :(
var object = js.context['myTopLevl'];
object['field1'] = 1;
// This call fails with a noSuchMethod because method2
// returns an int, oops
object.callMethod('method2', []).substr(1);
}

We knew then that dart:js was not the programming model we wanted. You had to use strings to access names from JavaScript — forget about finding issues at compile-time, and don’t even think about code completion! The implementation was expensive, too. It heavily relied on boxes and deep copies for most operations. So we continued drafting ideas in 2014 and 2015 until v0.6 of package:js was released.

// Access via `package:js` (2015)
import 'package:js/js.dart';

// Magic annotations allow us to declare API signatures:
@JS()
class MyObject {
external int get field1;
external void set field1(int value);
external String method2();
}

@JS()
external MyObject get myTopLevel;

void main() {
// Access to code is less error prone: analyzer can check that
// these symbols match a declaration, and we get code-completion too!
var object = myTopLevel;
object.field1 = 1;
// But types are not checked, this unsoundly invokes substring on an int
object.method2().substring(1);
}

With package:js we finally had an open API that was efficient and user friendly. You could sprinkle some annotations on abstract classes, and voila, you had access to JavaScript APIs. It all worked like magic, until it didn’t. There was a lot you couldn’t do with package:js: accessing browser APIs directly, renaming members, conversions, attaching Dart logic, and more. To compensate, we also shipped dart:js_util — a lightweight and efficient low-level API similar to dart:js, as a fallback. All the limitations in package:js really bothered us, but our hands were tied. We needed more from the Dart language to do better.

Around that time, we were already working on the biggest change to the language we have ever made — we were making Dart sound. Ironically, when we released the new type system with Dart 2.0 in 2018, interoperability got worse! Beyond those early limitations, that magic that made package:js special had a dark side — it couldn’t check the validity of types. This meant that our interoperability was a source of unsoundness in our otherwise sound language.

Then, our journey changed to focus on improving both Dart and JS-interop as a concerted effort. With clear principles (be idiomatic, expressive, compositional, precise, approachable, pragmatic, non-magical, and complete) we steered towards a design that anchored on typing and static dispatching, and that challenged the Dart language. What followed was a side-by-side evolution.

  • In 2019, Dart 2.7 added static extension methods. You could attach custom Dart logic to a JS-interop class and convert values, like a JS Promise into a Dart Future, without using wrappers.
  • In 2021, we released @staticInterop with package:js v0.6.4. At last, JS-interop was expressive enough — you could expose browser APIs that previously were exclusively managed by SDK libraries like dart:html.
  • In 2023, when we dropped unsound null safety in Dart 3.0, we could finally see the progress we had made, our designs and @staticInterop work made it clear we were ready to address the soundness gap we had for so long.

That year, we introduced compilation to WasmGC and leveraged JS-interop to run rich frameworks like Flutter web on it. This sparked work on JS Types to clearly define the Dart and JS boundary in the programming model and find a consistent way to work with JS in both Wasm and JS compilation targets. We also started the extension types language experiment — a feature launched in Dart 3.3 that bridges the gap between the Dart language and JS-interop. For years, JS-interop had behaviors, like type erasure, that didn’t match anything else in Dart. With extension types, JS-interop could finally be idiomatic and get the support it deserves in Dart development tools.

Despite the many shifts and turns along the way, one thing remained consistent throughout the entire decade: the active engagement of our Dart community. Community members took early steps testing and contributing to dart:js, then later influencing the design of package:js. They wrote tools to address feature gaps (package:js_wrapping), and experimented with ways to improve productivity by autogenerating Dart APIs (package:js_facade_gen, package:js_bindings, package:typings). Each contribution helped make Dart’s interop design better. To each of you out there, thank you for making this such an exciting adventure!

Finally, here we are in 2024. We released dart:js_interop in Dart 3.3 together with package:web, the newest solutions for JS interop in Dart that make compiling Flutter to Wasm possible.

// Access via `dart:js_interop` (2024)
import 'dart:js_interop';

// Declarations use extension types, which are very similar to package:js
// declarations. The main difference: they are statically dispatched.
extension type MyObject._(JSObject _) implements JSObject {
external int get field1;
external void set field1(int value);
external String method2();
}

@JS()
external MyObject get myTopLevel;

void main() {
var object = myTopLevel;
object.field1 = 1;
// At last, access is sound - this line fails with a type error
// when returning from method2.
object.method2().substring(1);
}
  • dart:js_interop is a static, sound, idiomatic, expressive, and consistent form of interop based on extension types that is capable of exposing any JavaScript or browser APIs.
  • package:web uses dart:js_interop to do what dart:html once did 13 years ago, but in a way that is supported both in JavaScript and WasmGC.

Today, we are excited to celebrate a new form of Dart/JS interop and the future it enables. Knowing our past, we are certain this isn’t the end of the journey, but an exciting point in our history.

We can’t wait to see what you’ll build with it!

--

--