| |

Dart 60 🎯 Web Interop (dart:js_interop): Interfacing with JavaScript and WASM

⏱️ Estimated read: 16 min | |

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 .

TypeRepresents
JSAnyAny non-nullish JavaScript value
JSNumberA JavaScript number
JSBooleanA JavaScript boolean
JSStringA JavaScript string
JSSymbolA JavaScript symbol
JSObjectAny JavaScript object
JSFunctionA JavaScript function
JSArrayA JavaScript array
JSPromiseA JavaScript promise
JSTypedArrayA JavaScript typed array
JSBoxedDartObjectA 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 typeDart type
JSNumbernum, int, double
JSBooleanbool
JSStringString
JSArray<T>List<T>
JSPromise<T>Future<T>
JSUint8ArrayUint8List
JSBoxedDartObjectOpaque 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

TypeRepresents
JSAnyAny non-nullish JS value
JSNumberA JS number
JSStringA JS string
JSBooleanA JS boolean
JSObjectAny JS object
JSFunctionA JS function
JSArrayA JS array
JSPromiseA JS promise
JSTypedArrayA JS typed array
JSBoxedDartObjectA boxed Dart value

The Conversions

FromToExtension
Dart StringJSStringtoJS
Dart intJSNumbertoJS
Dart ListJSArraytoJS
Dart FutureJSPromisetoJS
Dart FunctionJSFunctiontoJS
JSNumberDart inttoDartInt
JSStringDart StringtoDart
JSArrayDart ListtoDart
JSPromiseDart FuturetoDart

The Interop Declarations

DeclarationSyntax
Top-level@JS('name') external ...
Extension type@JS('Name') extension type X._(JSObject _) ...
Instance memberexternal ...
Static memberexternal static ...
Constructorexternal X(...)
Operatorexternal T operator [](int i)

The Browser APIs

ImportPurpose
package:web/web.dartThe DOM, window, fetch, storage
dart:js_interopThe JS types and conversions
dart.library.js_interopThe conditional import condition

The Wasm Compatibility

LibraryWasm-Ready
dart:js_interopYes
package:webYes
dart:htmlNo
dart:jsNo
package:jsNo

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

PitfallWhy It HappensFix
Type mismatchMissing conversionUse toJS or toDart
Dynamic call failsdart:js_interop is staticUse the static member
Wasm compilation failsLegacy library importedMigrate to package:web
is check failsJS types are erasedUse the conversion
Callback not convertedMissing toJSUse .toJS on the function
@JS() missingTop-level needs the annotationAdd the annotation
Object literal failsWrong constructor syntaxUse 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

ItemValue
dart:js_interopThe modern JS interop library
JSAnyThe top JS type
JSNumber, JSStringThe JS primitives
JSObjectAny JS object
toJSDart to JS conversion
toDartJS to Dart conversion
Extension typeThe interop declaration mechanism
@JS()The annotation for interop members
package:webThe browser API bindings
WasmThe modern compilation target

Key takeaways:

  • dart:js_interop replaces the legacy interop libraries. dart:js, package:js, and dart:html are 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 toJS extension converts Dart values to JS types, and the toDart extension converts back. The conversions are explicit, and the developer must use them .
  • The extension type declares the interop binding. The extension type syntax with the @JS() annotation binds a Dart type to a JavaScript global. The external members declare the JavaScript methods and properties .
  • The toJSBox extension wraps Dart objects. A Dart value that must be passed to JavaScript opaquely is wrapped in a JSBoxedDartObject. The wrapper is explicit and works on both targets .
  • package:web replaces dart:html. The browser APIs are now provided by package: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_interop conditional import is the correct check. The condition is true when the code is compiled to a web target, whether JavaScript or Wasm. The legacy dart.library.html condition is deprecated .
  • The Wasm compilation target requires the modern interop. The legacy libraries do not work on Wasm. The dart:js_interop library and the package:web library 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.


Was this helpful?


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!