From 3ce781fb8148ef0dcd7f9316b9fb149046127d91 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Fri, 12 Jan 2024 16:41:05 -0800 Subject: [PATCH] Add a section for JS types and reference it in the nav --- src/_data/side-nav.yml | 2 +- src/interop/js-interop/js-types.md | 189 +++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/interop/js-interop/js-types.md diff --git a/src/_data/side-nav.yml b/src/_data/side-nav.yml index 90cdc89d01..ce8bdd5d1d 100644 --- a/src/_data/side-nav.yml +++ b/src/_data/side-nav.yml @@ -251,7 +251,7 @@ - title: Overview permalink: /interop/js-interop - title: JS types - permalink: /interop/js-interop/reference + permalink: /interop/js-interop/js-types - title: Tutorials permalink: /interop/js-interop/tutorials - title: Past JS interop diff --git a/src/interop/js-interop/js-types.md b/src/interop/js-interop/js-types.md new file mode 100644 index 0000000000..1e23b77d67 --- /dev/null +++ b/src/interop/js-interop/js-types.md @@ -0,0 +1,189 @@ +--- +title: JS Types +description: Usage information about the core types in JS interop. +--- + +## Motivation + +Dart values and JS values belong to separate domains. When compiling to Wasm, +they belong in separate *runtimes* as well. As such, users should treat JS +values as foreign types. In order to provide types for JS values, we use the set +of JS types available in `dart:js_interop`. These are extension types prefixed +with `JS` and are called "JS types". + +Importantly, these types have compiler-specific representation types. This means +that their runtime type will differ based on whether code is compiled to Wasm or +JS. In order to interact with and examine these JS values, we use `external` +interop members. + +## 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 + - `JSArray` + - `JSPromise` + - `JSDataView` + - `JSTypedArray` + - JS typed arrays e.g. `JSUint8Array` + - `JSBoxedDartObject`, which allows users to box and pass Dart values + opaquely within the same Dart runtime + +Note that these JS types are *also* interop types. As such, users can and should +use them as representation types of other interface types. Users will likely +want to use `JSObject` for this purpose. + +// TODO(srujzs): Add a link to the interop type section in the syntax. +// TODO(srujzs): Should we link the type to the definition? It might be a bit +// annoying to do everywhere a JS type is mentioned. + +## Conversions + +Since we have separate domains, we will likely want to *convert* values between +one domain to the other. For example, we may want to convert a Dart `String` +into a JS string, which is represented by the JS type JSString. To do this, we +supply a number of `extension` members on various Dart types that will do the +conversion for you, usually labeled as or prefixed with `toJS`. Similarly, we +can convert JS values to Dart types. We supply a number of members on various JS +types to enable this conversion, usually labeled as or prefixed with `toDart`. +They are often marked `external`, and their implementation is usually +platform-dependent. Note that not all JS types have a conversion, and not all +Dart types have a conversion. + +In general, the conversion table looks like the following: + +| JS type | Dart type | +| ----------------------------------- | ---------------------------------------- | +| `JSNumber`, `JSBoolean`, `JSString` | `num`, `int`, `double`, `bool`, `String` | +| `JSExportedFunction` | `JSFunction` | +| `JSArray` | `List` | +| `JSPromise` | `Future` | +| Typed arrays e.g. `JSUint8Array` | `dart:typed_data` | +| `JSBoxedDartObject` | Opaque Dart value | + +**Important**: Conversions may have different costs depending on the compiler. +Prefer to only convert when you need to. Furthermore, conversions may or may not +produce a new value. This doesn’t matter for immutable values like numbers, but +does matter for types like `List`s. A conversion to a `JSArray` may produce a +new value by copying or may not, so do not rely on modifications to the +`JSArray` affecting the `List`. Typed array conversions have a similar +limitation. Look at the specific conversion function for more details. + +## Requirements on `external` declarations and `Function.toJS` + +In order to ensure type safety and consistency, we place requirements on what +types can flow into and out of JS. Passing arbitrary Dart values into JS is not +allowed. Instead, we require users to use a compatible interop type like a JS +type or a primitive, which would then be implicitly converted by the compiler. +For example, these would be allowed: + +```dart +@JS() +external void usePrimitives(String a, int b, double c, num d, bool e); +``` +```dart +@JS() +external JSArray useJsTypes(JSObject _, JSString __); +``` +```dart +extension type InteropType(JSObject _) {} + +@JS() +external InteropType get interopType; +``` + +whereas these would return an error: + +```dart +@JS() +external Function get function; +``` + +```dart +@JS() +external set list(List _); +``` + +These same requirements exist when we 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 e.g. `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 maybe +using `JSString` instead makes sense to avoid the conversion cost. + +## Compatibility/type checks and casts + +As mentioned above, the representation type of JS types may 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. Avoid casts between Dart types and interop types. In order to +type-check a JS value, use an interop member like `typeofEquals` or +`instanceOfString` that examines the JS value itself. + +Bad: + +```dart +void f(JSAny a) { + if (a is String) { … } +} +``` + +```dart +void f(JSObject o) { + if (o is JSObject) { … } +} +``` + +```dart +void f(JSString s) { + l as String; +} +``` + +Okay: + +```dart +void f(JSAny a) { + // Here we verify that `a` is a JS function, so the cast is okay. + if (a.typeofEquals('function')) { + a as JSFunction; + } +} +``` + +We may add lints to make runtime checks with JS interop types easier to avoid. + +## `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`, we map these +values to Dart `null`. Therefore a member like: + +```dart +@JS() +external JSObject? get value; +``` + +can be interpreted as returning a JS object, JS `null`, or `undefined`. + +**Important**: There is a subtle inconsistency with regards to `undefined` +between compiling to JS and Wasm. While compiling to JS treats `undefined` +values like Dart `null`, it doesn’t actually *change* the value itself. If you +an interop member returns `undefined` and you pass it back into JS, JS will see +`undefined` and not `null` when compiling to JS. When compiling to Wasm, this is +not the case, because we convert the value to Dart `null`, thereby losing +information on whether the original value was JS `null` or `undefined`. Avoid +writing code where this distinction matters by explicitly passing Dart `null` +instead to an interop member. Currently, there’s no platform-consistent way to +provide `undefined` to interop members or distinguish between JS `null` and +`undefined` values, but this will likely change in the future. \ No newline at end of file