Dart 60 🎯 Web Interop (dart:js_interop): Interfacing with JavaScript and WASM
Dart’s web platform is unique in that it compiles to two different targets: JavaScript for the traditional browser runtime, and WebAssembly for the modern, high-performance runtime. For years, these two targets had separate interop mechanisms, and code that worked on one did not work on the other. The dart:js_interop library unifies them. It provides a single set of types, conversions, and declarations that work whether the code is compiled to JavaScript or to Wasm . The result is an interop layer that is sound, type-safe, and future-proof, and a codebase that does not need to be rewritten when the compilation target changes.
Key point: dart:js_interop is not a wrapper around JavaScript. It is a type system that describes JavaScript values to the Dart compiler. The types are erased at runtime, and the static guarantees are what matter. A JSString is not a Dart String; it is a Dart type that represents a JavaScript string, and the toDart conversion is what bridges the two domains.
Why dart:js_interop exists
The legacy interop problem. Dart’s original interop libraries — dart:js, package:js, and dart:html — were designed for the JavaScript compilation target. They relied on dynamic dispatch, lacked soundness guarantees, and did not work when compiling to Wasm . The legacy libraries are deprecated as of Dart 3.7, and the modern replacement is dart:js_interop .
The soundness problem. package:js did not check the return types of external members, which meant a wrong declaration produced a runtime error rather than a compile-time one. dart:js_interop is sound: the types are checked at compile time, and a mismatch is a compile error .
The Wasm problem. The WebAssembly compilation target does not have access to the DOM or the JavaScript runtime in the same way. The legacy libraries assumed a JavaScript environment and could not run on Wasm. dart:js_interop is designed for both targets, and it is the only interop mechanism that works when compiling to Wasm .
The type problem. JavaScript values and Dart values belong to separate domains. A JavaScript number is not a Dart num, and a JavaScript string is not a Dart String. dart:js_interop provides the JSNumber, JSString, and the rest of the JS types to represent the JavaScript values, and the toJS and toDart conversions to move between the domains .
The browser API problem. The dart:html library provided the browser APIs, but it was JavaScript-specific and could not work on Wasm. The package:web library replaces it with a set of interop types that bind the browser APIs to dart:js_interop. The types are sound and work on both targets .
The static interop problem. The legacy interop allowed dynamic dispatch and dynamic calls. dart:js_interop is static: the members are resolved at compile time, and a dynamic call does not work. The static approach is what enables the soundness and the Wasm compatibility .
The conversion problem. The two domains have separate types, and the conversions are explicit. The toJS extension on Dart types converts to the JS types, and the toDart extension on JS types converts back. The conversions are the boundary, and the developer must use them .
a. JS types and conversions
The dart:js_interop library defines a hierarchy of types that represent JavaScript values. The types are prefixed with JS, and they form a natural hierarchy from JSAny down to the specific types .
The type hierarchy
The top type is JSAny, which represents any non-nullish JavaScript value. Below it are the primitives: JSNumber, JSBoolean, JSString, and JSSymbol. Below them is JSObject, which represents any JavaScript object, and its subtypes: JSFunction, JSArray, JSPromise, JSDataView, and the typed arrays .
| Type | Represents |
|---|---|
JSAny | Any non-nullish JavaScript value |
JSNumber | A JavaScript number |
JSBoolean | A JavaScript boolean |
JSString | A JavaScript string |
JSSymbol | A JavaScript symbol |
JSObject | Any JavaScript object |
JSFunction | A JavaScript function |
JSArray | A JavaScript array |
JSPromise | A JavaScript promise |
JSTypedArray | A JavaScript typed array |
JSBoxedDartObject | A Dart value wrapped for JavaScript |
The conversions
The conversions move values between the Dart domain and the JavaScript domain. The toJS extensions convert Dart values to JS types, and the toDart extensions convert JS types to Dart values .
// Dart to JS
String dartString = 'hello';
JSString jsString = dartString.toJS;
int dartInt = 42;
JSNumber jsNumber = dartInt.toJS;
// JS to Dart
JSNumber jsNum = ...;
int integer = jsNum.toDartInt;
double doubleValue = jsNum.toDartDouble;
JSString jsStr = ...;
String dartStr = jsStr.toDart;
The conversion table is the bridge between the domains.
| JS type | Dart type |
|---|---|
JSNumber | num, int, double |
JSBoolean | bool |
JSString | String |
JSArray<T> | List<T> |
JSPromise<T> | Future<T> |
JSUint8Array | Uint8List |
JSBoxedDartObject | Opaque Dart value |
The JSBoxedDartObject
A Dart value that must be passed to JavaScript opaquely is wrapped in a JSBoxedDartObject. The value is not converted; it is boxed so that JavaScript can hold a reference to it .
final dartObject = SomeDartClass();
final boxed = dartObject.toJSBox;
// Pass boxed to JavaScript.
// Later, unbox it.
final original = boxed.toDart;
The toJSBox extension replaces the older toJS extension, which allowed Dart objects to flow into JavaScript unwrapped on the JS backends. The explicit wrapper is safer and works on both targets .
The ExternalDartReference
The ExternalDartReference is an alternative to JSBoxedDartObject for passing Dart values opaquely. The difference is in the lifecycle and the trade-offs, which are documented in the API docs .
The conversion rules
Not every JS type has a Dart conversion, and not every Dart type has a JS conversion. The conversions are defined for the common cases, and a custom conversion is needed for the rest. The toJS and toDart extensions are the primary mechanisms .
b. Interop declarations
The dart:js_interop library uses extension types to declare the bindings to JavaScript APIs. An extension type is a zero-cost wrapper around a JS type, and the external members declare the JavaScript members .
The extension type
An extension type is declared with the extension type syntax, and the representation type is a JS type.
import 'dart:js_interop';
@JS('Date')
extension type JsDate._(JSObject _) implements JSObject {
external JsDate();
external double getTime();
external int getFullYear();
}
The @JS('Date') annotation names the JavaScript global. The _ constructor is the representation constructor, and the external members declare the JavaScript methods and getters .
The top-level members
Top-level interop members are declared with external and the @JS() annotation.
@JS('console.log')
external void log(JSAny? message);
@JS('JSON.stringify')
external JSString stringify(JSAny? value);
The @JS() annotation is required for top-level declarations to distinguish them from other external declarations like dart:ffi .
The rename
The @JS() annotation can rename a member when the Dart name differs from the JavaScript name.
@JS('getElementById')
external JSObject getElementById(JSString id);
The annotation maps the Dart name to the JavaScript name .
The operators
An interop type can declare the [] and []= operators.
extension type JsArray<T extends JSAny?>(JSArray<T> _) implements JSArray<T> {
external T operator [](int index);
external void operator []=(int index, T value);
}
Other JavaScript operators are exposed through utility functions in dart:js_interop .
The static members
Static members are declared with external static.
extension type JsMath._(JSObject _) implements JSObject {
external static double max(double a, double b);
external static double get PI;
}
The static member is called as JsMath.max(1, 2) .
The object literal constructor
An external named-argument constructor is an object literal constructor.
extension type Options._(JSObject _) implements JSObject {
external Options({String? name, int? age});
}
The constructor creates a JavaScript object with the named properties .
The callback
A Dart function can be converted to a JavaScript function with the toJS extension on Function.
final jsFunction = (JSAny? value) {
print(value);
}.toJS;
// Pass jsFunction to JavaScript.
The JSExportedDartFunction is the type that represents the converted function .
The @JS() annotation
The @JS() annotation is required for top-level declarations and optional for interop types and extension members. The compiler infers the interop from the representation type or the on type .
c. Browser APIs and package:web
The dart:html library is deprecated, and the browser APIs are now provided by package:web. The library is a set of interop types that bind the DOM, the window, the fetch API, and the rest of the browser platform to dart:js_interop .
The package:web import
The browser APIs are imported from package:web.
import 'package:web/web.dart' as web;
void main() {
final div = web.document.createElement('div') as web.HTMLDivElement;
div.textContent = 'Hello, world!';
web.document.body!.appendChild(div);
}
The types are interop types, and the members are external .
The conditional import
The correct way to differentiate between the native and web implementations is the dart.library.js_interop conditional import.
export 'src/hw_none.dart'
if (dart.library.io) 'src/hw_io.dart'
if (dart.library.js_interop) 'src/hw_web.dart';
The dart.library.js_interop condition is true when the code is compiled to a web target, whether JavaScript or Wasm .
The static dispatch
The package:web types are extension types, and the members are resolved statically. A dynamic call does not work, and the virtual dispatch that dart:html allowed is not available. The code must be migrated to use the static members .
The stream helpers
The package:web library does not provide the stream wrappers that dart:html had. The helpers are in a separate library, or the developer writes them .
The Wasm compatibility
The package:web library is Wasm-ready. It does not import the legacy libraries, and the interop types work on both targets .
The wasm-ready check
A package is “wasm-ready” if it does not import the non-Wasm-compliant libraries like dart:html and dart:js. The wasm-ready tool checks the imports .
Complete Example Session
// ============================================
// PART 1: THE BASIC CONVERSIONS
// ============================================
import 'dart:js_interop';
void main() {
final dartString = 'hello';
final jsString = dartString.toJS;
final jsNumber = 42.toJS;
final dartInt = jsNumber.toDartInt;
print(dartInt); // 42
}
// ============================================
// PART 2: THE INTEROP EXTENSION TYPE
// ============================================
import 'dart:js_interop';
@JS('Date')
extension type JsDate._(JSObject _) implements JSObject {
external JsDate();
external double getTime();
external int getFullYear();
}
void main() {
final date = JsDate();
print(date.getFullYear());
}
// ============================================
// PART 3: THE TOP-LEVEL DECLARATION
// ============================================
import 'dart:js_interop';
@JS('console.log')
external void log(JSAny? message);
void main() {
log('Hello from Dart'.toJS);
}
// ============================================
// PART 4: THE CALLBACK
// ============================================
import 'dart:js_interop';
@JS('setTimeout')
external void setTimeout(JSFunction callback, int delay);
void main() {
final callback = (() {
print('Timeout fired');
}).toJS;
setTimeout(callback, 1000);
}
// ============================================
// PART 5: THE BROWSER API WITH package:web
// ============================================
import 'package:web/web.dart' as web;
void main() {
final div = web.document.createElement('div') as web.HTMLDivElement;
div.textContent = 'Hello, world!';
div.style.backgroundColor = 'lightblue';
web.document.body!.appendChild(div);
}
// ============================================
// PART 6: THE FETCH API
// ============================================
import 'dart:js_interop';
import 'package:web/web.dart' as web;
Future<void> fetchData() async {
final response = await web.window.fetch('https://api.example.com/data'.toJS).toDart;
final json = await response.json().toDart;
print(json);
}
// ============================================
// PART 7: THE TYPED ARRAY
// ============================================
import 'dart:js_interop';
import 'dart:typed_data';
void main() {
final dartBytes = Uint8List.fromList([1, 2, 3, 4]);
final jsBytes = dartBytes.toJS;
final backToDart = jsBytes.toDart;
print(backToDart); // [1, 2, 3, 4]
}
// ============================================
// PART 8: THE BOXED DART OBJECT
// ============================================
import 'dart:js_interop';
class User {
final String name;
User(this.name);
}
@JS('processUser')
external void processUser(JSAny? user);
void main() {
final user = User('Alice');
processUser(user.toJSBox);
}
// ============================================
// PART 9: THE CONDITIONAL IMPORT
// ============================================
// lib/src/storage_none.dart
abstract class Storage {
Future<String?> read(String key);
}
// lib/src/storage_io.dart
import 'dart:io';
class Storage implements Storage {
Future<String?> read(String key) async => File(key).readAsString();
}
// lib/src/storage_web.dart
import 'package:web/web.dart' as web;
class Storage implements Storage {
Future<String?> read(String key) async {
return web.window.localStorage.getItem(key)?.toDart;
}
}
// lib/storage.dart
export 'src/storage_none.dart'
if (dart.library.io) 'src/storage_io.dart'
if (dart.library.js_interop) 'src/storage_web.dart';
// ============================================
// PART 10: THE WASM COMPILATION
// ============================================
// dart compile wasm main.dart -o main.wasm
// The dart:js_interop code compiles to Wasm.
// The legacy libraries do not.
// The package:web library is wasm-ready.
The ten parts showed the basic conversions, the interop extension type, the top-level declaration, the callback, the browser API, the fetch API, the typed array, the boxed Dart object, the conditional import, and the Wasm compilation.
Quick Reference
The JS Types
| Type | Represents |
|---|---|
JSAny | Any non-nullish JS value |
JSNumber | A JS number |
JSString | A JS string |
JSBoolean | A JS boolean |
JSObject | Any JS object |
JSFunction | A JS function |
JSArray | A JS array |
JSPromise | A JS promise |
JSTypedArray | A JS typed array |
JSBoxedDartObject | A boxed Dart value |
The Conversions
| From | To | Extension |
|---|---|---|
Dart String | JSString | toJS |
Dart int | JSNumber | toJS |
Dart List | JSArray | toJS |
Dart Future | JSPromise | toJS |
Dart Function | JSFunction | toJS |
JSNumber | Dart int | toDartInt |
JSString | Dart String | toDart |
JSArray | Dart List | toDart |
JSPromise | Dart Future | toDart |
The Interop Declarations
| Declaration | Syntax |
|---|---|
| Top-level | @JS('name') external ... |
| Extension type | @JS('Name') extension type X._(JSObject _) ... |
| Instance member | external ... |
| Static member | external static ... |
| Constructor | external X(...) |
| Operator | external T operator [](int i) |
The Browser APIs
| Import | Purpose |
|---|---|
package:web/web.dart | The DOM, window, fetch, storage |
dart:js_interop | The JS types and conversions |
dart.library.js_interop | The conditional import condition |
The Wasm Compatibility
| Library | Wasm-Ready |
|---|---|
dart:js_interop | Yes |
package:web | Yes |
dart:html | No |
dart:js | No |
package:js | No |
Best Practices
✅ Do This:
// Use the toJS and toDart conversions.
final jsString = 'hello'.toJS; // ✅
final dartString = jsString.toDart; // ✅
// Use the extension type for interop declarations.
@JS('Date')
extension type JsDate._(JSObject _) implements JSObject { ... } // ✅
// Use the @JS() annotation for top-level declarations.
@JS('console.log')
external void log(JSAny? message); // ✅
// Use package:web for browser APIs.
import 'package:web/web.dart' as web; // ✅
// Use the dart.library.js_interop conditional import.
if (dart.library.js_interop) 'src/storage_web.dart'; // ✅
// Use toJSBox for opaque Dart objects.
processUser(user.toJSBox); // ✅
// Compile to Wasm for the modern web.
// dart compile wasm main.dart // ✅
❌ Don’t Do This:
// Don't use dart:html or dart:js.
import 'dart:html'; // ❌ Deprecated, not Wasm-compatible
// Don't use package:js.
import 'package:js/js.dart'; // ❌ Deprecated
// Don't rely on dynamic dispatch.
final dynamic result = jsObject.someMethod(); // ❌ Not supported
// Don't use is checks on JS types.
if (jsValue is JSString) { } // ❌ Runtime types differ
// Don't mix Dart values and JS values.
print(jsString.length); // ❌ JSString does not have Dart members
// Don't forget the conversion.
final length = jsString.length; // ❌ Use jsString.toDart.length
// Don't use the legacy interop.
// Migrate to dart:js_interop. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Type mismatch | Missing conversion | Use toJS or toDart |
| Dynamic call fails | dart:js_interop is static | Use the static member |
| Wasm compilation fails | Legacy library imported | Migrate to package:web |
is check fails | JS types are erased | Use the conversion |
| Callback not converted | Missing toJS | Use .toJS on the function |
@JS() missing | Top-level needs the annotation | Add the annotation |
| Object literal fails | Wrong constructor syntax | Use external named args |
Real-World Examples
1. Basic Conversion
final jsString = 'hello'.toJS;
final dartString = jsString.toDart;
2. Interop Type
@JS('Date')
extension type JsDate._(JSObject _) implements JSObject {
external JsDate();
external int getFullYear();
}
3. Top-Level Member
@JS('console.log')
external void log(JSAny? message);
4. Callback
final callback = (() => print('fired')).toJS;
5. Browser API
import 'package:web/web.dart' as web;
final div = web.document.createElement('div') as web.HTMLDivElement;
6. Fetch
final response = await web.window.fetch('https://api.example.com'.toJS).toDart;
7. Typed Array
final jsBytes = Uint8List.fromList([1, 2, 3]).toJS;
8. Boxed Object
processUser(user.toJSBox);
9. Conditional Import
if (dart.library.js_interop) 'src/storage_web.dart';
10. Wasm Compilation
dart compile wasm main.dart
Visual
The JS Type Hierarchy
┌────────────────────────────────────────────────────────────┐
│ THE JS TYPE HIERARCHY │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ JSAny │ │
│ │ Any non-nullish JavaScript value │ │
│ │ │ │
│ │ ┌─────────────┬─────────────┬─────────────┐ │ │
│ │ │ JSNumber │ JSBoolean │ JSString │ │ │
│ │ └─────────────┴─────────────┴─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ JSObject │ │ │
│ │ │ Any JavaScript object │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────┬──────────┬──────────┐ │ │ │
│ │ │ │JSFunction│ JSArray │JSPromise │ │ │ │
│ │ │ └──────────┴──────────┴──────────┘ │ │ │
│ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ JSTypedArray │ │ │ │
│ │ │ │ JSUint8Array, JSInt32Array...│ │ │ │
│ │ │ └─────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The types are reified differently on JS and Wasm. │
│ Use the conversions, not is checks. │
└────────────────────────────────────────────────────────────┘
The Conversion Boundary
┌────────────────────────────────────────────────────────────┐
│ THE CONVERSION BOUNDARY │
│ │
│ DART DOMAIN JS DOMAIN │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ String │──toJS──▶│ JSString │ │
│ │ int │──toJS──▶│ JSNumber │ │
│ │ bool │──toJS──▶│ JSBoolean │ │
│ │ List<T> │──toJS──▶│ JSArray<T> │ │
│ │ Future<T> │──toJS──▶│ JSPromise<T> │ │
│ │ Function │──toJS──▶│ JSFunction │ │
│ └──────────────────┘ └──────────────────┘ │
│ ▲ │ │
│ │ │ │
│ └──────────toDart───────────┘ │
│ │
│ The conversions are explicit. │
│ A JSString is not a String. │
└────────────────────────────────────────────────────────────┘
The Interop Declaration
┌────────────────────────────────────────────────────────────┐
│ THE INTEROP DECLARATION │
│ │
│ @JS('Date') │
│ extension type JsDate._(JSObject _) implements JSObject { │
│ external JsDate(); │
│ external double getTime(); │
│ external int getFullYear(); │
│ } │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @JS('Date') ← The JavaScript global name │ │
│ │ extension type ← The Dart interop type │ │
│ │ JSObject _ ← The representation type │ │
│ │ external ← The JavaScript member │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Usage: │
│ final date = JsDate(); │
│ print(date.getFullYear()); │
│ │
│ The Dart call compiles to a JavaScript call. │
└────────────────────────────────────────────────────────────┘
The Wasm Compilation
┌────────────────────────────────────────────────────────────┐
│ THE TWO COMPILATION TARGETS │
│ │
│ DART SOURCE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ import 'dart:js_interop'; │ │
│ │ import 'package:web/web.dart' as web; │ │
│ │ │ │
│ │ final div = web.document.createElement('div'); │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ v v │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ dart compile js │ │ dart compile wasm│ │
│ │ (JavaScript) │ │ (WebAssembly) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Both targets work with dart:js_interop. │
│ The legacy libraries only work with JavaScript. │
│ The Wasm target is faster and more modern. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
dart:js_interop | The modern JS interop library |
JSAny | The top JS type |
JSNumber, JSString | The JS primitives |
JSObject | Any JS object |
toJS | Dart to JS conversion |
toDart | JS to Dart conversion |
| Extension type | The interop declaration mechanism |
@JS() | The annotation for interop members |
package:web | The browser API bindings |
| Wasm | The modern compilation target |
Key takeaways:
dart:js_interopreplaces the legacy interop libraries.dart:js,package:js, anddart:htmlare deprecated as of Dart 3.7. The modern library is the only one that works with both the JavaScript and Wasm compilation targets .- The JS types represent JavaScript values. The
JSNumber,JSString,JSObject, and the rest are the Dart types that mirror the JavaScript types. The types are erased at runtime, and the static guarantees are what matter . - The conversions move values between the domains. The
toJSextension converts Dart values to JS types, and thetoDartextension converts back. The conversions are explicit, and the developer must use them . - The extension type declares the interop binding. The
extension typesyntax with the@JS()annotation binds a Dart type to a JavaScript global. Theexternalmembers declare the JavaScript methods and properties . - The
toJSBoxextension wraps Dart objects. A Dart value that must be passed to JavaScript opaquely is wrapped in aJSBoxedDartObject. The wrapper is explicit and works on both targets . package:webreplacesdart:html. The browser APIs are now provided bypackage:web, which is a set of interop types that bind the DOM, the window, the fetch API, and the rest of the browser platform .- The
dart.library.js_interopconditional import is the correct check. The condition is true when the code is compiled to a web target, whether JavaScript or Wasm. The legacydart.library.htmlcondition is deprecated . - The Wasm compilation target requires the modern interop. The legacy libraries do not work on Wasm. The
dart:js_interoplibrary and thepackage:weblibrary are wasm-ready, and the code that uses them compiles to Wasm . - The interop is static, not dynamic. The members are resolved at compile time, and a dynamic call does not work. The static approach is what enables the soundness and the Wasm compatibility .
Remember: dart:js_interop is the unified interop library for Dart’s web platform. It provides the JS types that represent JavaScript values, the conversions that move between the Dart and JavaScript domains, and the extension types that declare the bindings to JavaScript APIs. The library works with both the JavaScript and Wasm compilation targets, and it replaces the legacy dart:js, package:js, and dart:html libraries. The package:web library provides the browser APIs, and the dart.library.js_interop conditional import differentiates the web target from the native target. The result is an interop layer that is sound, type-safe, and future-proof, and a codebase that does not need to be rewritten when the compilation target changes.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!