JS types
Usage information about the core types in JS interop.
Dart values and JS values belong to separate language
domains. When compiling to
Wasm, they execute in
separate runtimes as well. As such, you should
treat JS values as foreign types. To provide Dart types for
JS values,
dart:js_interop
exposes a set of types prefixed with JS called
"JS types". These types are used to distinguish between Dart
values and JS values at compile-time.
Importantly, these types are reified differently based on
whether you compile to Wasm or JS. This means that their
runtime type will differ, and therefore you
can't use is checks and
as casts. In order to interact with and examine these JS values,
you should use
external
interop members or
conversions.
Type hierarchy
#JS types form a natural type hierarchy:
-
Top type:
JSAny, which is any non-nullish JS value-
Primitives:
JSNumber,JSBoolean,JSString,JSSymbol,JSBigInt -
JSObject, which is any JS object-
JSFunction-
JSExportedDartFunction, which represents a Dart callback that was converted to a JS function
-
JSArrayJSPromiseJSDataView-
JSTypedArray-
JS typed arrays like
JSUint8Array
-
JS typed arrays like
-
JSBoxedDartObject, which allows users to box and pass Dart values opaquely within the same Dart runtime-
From Dart 3.4 onwards, the type
ExternalDartReferenceindart:js_interopalso allows users to pass Dart values opaquely, but is not a JS type. Learn more about the tradeoffs between each option here.
-
From Dart 3.4 onwards, the type
-
-
Primitives:
You can find the definition of each type in the
dart:js_interop API docs.
Conversions
#
To use a value from one domain to another, you will likely
want to convert the value to the corresponding type
of the other domain. For example, you might want to convert
a Dart List<JSString> into a JS array of
strings, which is represented by the JS type
JSArray<JSString>, so that you can pass
the array to a JS interop API.
Dart supplies a number of conversion members on various Dart types and JS types to convert the values between the domains for you.
Members that convert values from Dart to JS usually start
with toJS:
String str = 'hello world';
JSString jsStr = str.toJS;
Members that convert values from JS to Dart usually start
with toDart:
JSNumber jsNum = ...;
int integer = jsNum.toDartInt;
Not all JS types have a conversion, and not all Dart types have a conversion. Generally, the conversion table looks like the following:
dart:js_interop type |
Dart type |
|---|---|
JSNumber, JSBoolean,
JSString
|
num, int,
double, bool,
String
|
JSExportedDartFunction |
Function |
JSArray<T extends JSAny?> |
List<T extends JSAny?> |
JSPromise<T extends JSAny?>
|
Future<T extends JSAny?> |
Typed arrays like JSUint8Array |
Typed lists from dart:typed_data |
JSBoxedDartObject |
Opaque Dart value |
ExternalDartReference |
Opaque Dart value |
Requirements on external declarations and
Function.toJS
#
In order to ensure type safety and consistency, the compiler
places requirements on what types can flow into and out of
JS. Passing arbitrary Dart values into JS is not allowed.
Instead, the compiler requires users to use a compatible
interop type, ExternalDartReference, or a
primitive, which would then be implicitly converted by the
compiler. For example, these would be allowed:
@JS()
external void primitives(String a, int b, double c, num d, bool e);
@JS()
external JSArray jsTypes(JSObject _, JSString __);
extension type InteropType(JSObject _) implements JSObject {}
@JS()
external InteropType get interopType;
@JS()
external void externalDartReference(ExternalDartReference _);
Whereas these would return an error:
@JS()
external Function get function;
@JS()
external set list(List _);
These same requirements exist when you use
Function.toJS
to make a Dart function callable in JS. The values that flow
into and out of this callback must be a compatible interop
type or a primitive.
If you use a Dart primitive like String, an
implicit conversion happens in the compiler to convert that
value from a JS value to a Dart value. If performance is
critical and you don’t need to examine the contents of the
string, then using JSString instead to avoid
the conversion cost might make sense like in the second
example.
Compatibility, type checks, and casts
#
The runtime type of JS types might differ based on the
compiler. This affects runtime type-checking and casts.
Therefore, almost always avoid is checks where
the value is an interop type or where the target type is an
interop type:
On WebAssembly, all JS interop types (such as
JSString, JSArray, and
user-defined extension types) share a single underlying
runtime representation (typically externref).
Because Dart's extension types are erased at compile time, a
runtime check like value is JSString cannot
distinguish a string from any other JS value, and will
evaluate to true for almost any JS value,
leading to silent bugs.
void f(JSAny a) {
if (a is String) { … }
}
void f(JSAny a) {
if (a is JSObject) { … }
}
Also, avoid casts between Dart types and interop types:
void f(JSString s) {
s as String;
}
To type-check a JS value, use an interop member like
typeofEquals
or
instanceOfString
that examines the JS value itself:
void f(JSAny a) {
// Here `a` is verified to be a JS function, so the cast is okay.
if (a.typeofEquals('function')) {
a as JSFunction;
}
}
From Dart 3.4 onwards, you can use the
isA
helper function to check whether a value is any interop
type:
void f(JSAny a) {
if (a.isA<JSString>()) {} // `typeofEquals('string')`
if (a.isA<JSArray>()) {} // `instanceOfString('Array')`
if (a.isA<CustomInteropType>()) {} // `instanceOfString('CustomInteropType')`
}
Depending on the type parameter, it'll transform the call into the appropriate type-check for that type.
To avoid invalid type checks with JS interop types, enable
the
invalid_runtime_check_with_js_interop_types
lint rule.
null vs undefined
#
JS has both a null and an
undefined value. This is in contrast with Dart,
which only has null. In order to make JS values
more ergonomic to use, if an interop member were to return
either JS null or undefined, the
compiler maps these values to Dart null.
Therefore a member like value in the following
example can be interpreted as returning a JS object, JS
null, or undefined:
@JS()
external JSObject? get value;
If the return type was not declared as nullable, then the
program will throw an error if the value returned was JS
null or undefined to ensure
soundness.
JSBoxedDartObject vs
ExternalDartReference
#
From Dart 3.4 onwards, both
JSBoxedDartObject
and
ExternalDartReference
can be used to pass opaque references to Dart
Objects through JavaScript. However,
JSBoxedDartObject wraps the opaque reference in
a JavaScript object, while
ExternalDartReference is the reference itself
and therefore is not a JS type.
Use JSBoxedDartObject if you need a JS type or
if you need extra checks to make sure Dart values don't get
passed to another Dart runtime. For example, if the Dart
object needs to be placed in a JSArray or
passed to an API that accepts a JSAny, use
JSBoxedDartObject. Use
ExternalDartReference
otherwise as it will be faster.
See
toExternalReference
and
toDartObject
to convert to and from an
ExternalDartReference.