Skip to content

Commit

Permalink
Cleanup wording to be clearer
Browse files Browse the repository at this point in the history
  • Loading branch information
srujzs committed Feb 12, 2024
1 parent 51ab02f commit 83a613f
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 65 deletions.
18 changes: 9 additions & 9 deletions src/content/interop/js-interop/js-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ TODO (srujzs): Should we add a tree diagram instead for JS types?
## 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
value to the corresponding type of the other domain. For example, you may 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.
Expand Down Expand Up @@ -75,18 +75,18 @@ Generally, the conversion table looks like the following:
| JS type | Dart type |
|-------------------------------------|------------------------------------------|
| `JSNumber`, `JSBoolean`, `JSString` | `num`, `int`, `double`, `bool`, `String` |
| `JSExportedFunction` | `Function` |
| `JSExportedDartFunction` | `Function` |
| `JSArray<T extends JSAny?>` | `List<T extends JSAny?>` |
| `JSPromise<T extends JSAny?>` | `Future<T extends JSAny?>` |
| Typed arrays like `JSUint8Array` | Typed arrays from `dart:typed_data` |
| Typed arrays like `JSUint8Array` | Typed lists from `dart:typed_data` |
| `JSBoxedDartObject` | Opaque Dart value |

{:.table .table-striped}
</div>

:::warning
There can be inconsistencies in both performance and semantics for conversions
when compiling to JavaScript vs Wasm. Conversions might have different costs
when compiling to JavaScript vs Wasm. Conversions may have different costs
depending on the compiler, so prefer to only convert values if you need to.
Conversions also may or may not produce a new value. This doesn’t matter for
immutable values like numbers, but does matter for types like `List`. Depending
Expand All @@ -102,8 +102,8 @@ specific conversion function for more details.
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 like a JS type or a primitive, which would then be implicitly converted by
the compiler. For example, these would be allowed:
type or a primitive, which would then be implicitly converted by the compiler.
For example, these would be allowed:

```dart tag=good
@JS()
Expand Down Expand Up @@ -141,7 +141,7 @@ 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
`JSString` instead to avoid the conversion cost may make sense like in the
second example.

## Compatibility, type checks, and casts
Expand Down Expand Up @@ -201,8 +201,8 @@ TODO: Add a link to and an example using `isA` once it's in a dev release. Users
should prefer that method if it's available.
{% endcomment %}

Dart might add lints to make runtime checks with JS interop types easier to
avoid. See issue [#4841] for more details.
Dart may add lints to make runtime checks with JS interop types easier to avoid.
See issue [#4841] for more details.

## `null` vs `undefined`

Expand Down
27 changes: 17 additions & 10 deletions src/content/interop/js-interop/mock.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: How to mock JavaScript interop objects.
title: How to mock JavaScript interop objects
---

{{site.why.learn}}
Expand All @@ -16,7 +16,7 @@ be used. This [limitation is true for extension members] as well, and therefore
instance extension type or extension members can't be mocked.

While this applies to any non-`external` extension type member, `external`
interop members are special as they invoke members on a JS value. For example:
interop members are special as they invoke members on a JS value.

```dart
extension type Date(JSObject _) implements JSObject {
Expand All @@ -29,7 +29,7 @@ As discussed in the [Usage] section, calling `getDay()` will result in calling
different *implementation* of `getDay` can be called.

In order to do this, there should be some mechanism of creating a JS object that
has a property `getDay` which when called, calls the Dart function. A simple way
has a property `getDay` which when called, calls a Dart function. A simple way
is to create a JS object and set the property `getDay` to a converted callback
e.g.

Expand All @@ -51,7 +51,8 @@ import 'dart:js_interop';
import 'package:expect/minitest.dart';
// The Dart class must have `@JSExport` on it or one of its instance members.
// The Dart class must have `@JSExport` on it or at least one of its instance
// members.
@JSExport()
class FakeCounter {
int value = 0;
Expand All @@ -77,13 +78,16 @@ void main() {
// Returns a JS object whose properties call the relevant instance members in
// `fakeCounter`.
var counter = createJSInteropWrapper<FakeCounter>(fakeCounter) as Counter;
// Calls `FakeCounter.value`.
expect(counter.value, 0);
// Renamed member in the fake gets called.
// `FakeCounter.renamedIncrement` is renamed to `increment`, so it gets
// called.
counter.increment();
expect(counter.value, 1);
expect(fakeCounter.value, 1); // Dart object gets modified
expect(fakeCounter.value, 1);
// Changes in the fake affect the wrapper and vice-versa.
fakeCounter.value = 0;
expect(counter.value, 0); // Changes in Dart object affect the exported object
expect(counter.value, 0);
counter.decrement();
// Because `Counter.decrement` is non-`external`, we never called
// `FakeCounter.decrement`.
Expand All @@ -96,8 +100,8 @@ void main() {
`@JSExport` allows you to declare a class that can be used in
`createJSInteropWrapper`. `createJSInteropWrapper` will create an object literal
that maps each of the class' instance member names (or renames) to a JS callback
that triggers the instance's class member when called. In the above example,
getting and setting `counter.value` gets and sets `fakeCounter.value`.
that triggers the instance member when called. In the above example, getting and
setting `counter.value` gets and sets `fakeCounter.value`.

You can specify only some members of a class to be exported by omitting the
annotation from the class and instead only annotate the specific members. You
Expand All @@ -106,12 +110,15 @@ the documentation of [`@JSExport`].

Note that this mechanism isn't specific to testing only. You can use this to
provide a JS interface for an arbitrary Dart object, allowing you to essentially
*export* Dart objects to JS.
*export* Dart objects to JS with a predefined interface.

{% comment %}
TODO: Add more documentation in the dartdoc for `@JSExport`.
TODO: Should we add a section on general testing? We can't really mock
non-instance members unless the user explicitly replaces the real API in JS.

TODO: Expose this once the link works:
[extension types]: /language/extension-types
{% endcomment %}

[Usage]: /interop/js-interop/usage
Expand Down
96 changes: 50 additions & 46 deletions src/content/interop/js-interop/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,35 @@ from them using an explicit, idiomatic syntax.
Typically, you access a JavaScript API by making it available somewhere within
the [global JS scope]. To call and receive JS values from this API, you use
[`external` interop members](#interop-members). In order to construct and
provide types for JS values, you use and declare [interop types]
(#interop-types), which also contain interop members. To pass Dart values like
`List`s or `Function` to interop members or convert from JS values to Dart
values, you will use [conversion functions] unless the interop member is
[declared with a primitive].
provide types for JS values, you use and declare
[interop types](#interop-types), which also contain interop members. To pass
Dart values like `List`s or `Function` to interop members or convert from JS
values to Dart values, you use [conversion functions] unless the interop member
[contains a primitive type].

## Interop types

When interacting with a JS value, you need to provide a Dart type for it. You
can do this by either using or declaring an interop type. Interop types are
either a ["JS type"] provided by Dart or an [extension type] wrapping an interop
type. Interop types allow you to provide an interface for a JS value and lets
you declare interop APIs for its members. They are also used in the signature
of other interop APIs.
type.

Interop types allow you to provide an interface for a JS value and lets you
declare interop APIs for its members. They are also used in the signature of
other interop APIs.

```dart
extension type Window(JSObject _) implements JSObject {}
```

`Window` here is an interop type for an arbitrary `JSObject`. There is no
runtime guarantee that `Window` is actually a JS [`Window`]. See
[the extension types documentation] for more info on the runtime semantics of
extension types. There also is no conflict with any other interop interface
that is defined for the same value. If you do want to check that `Window` is
actually a JS `Window`, you can [check the type of the JS value through
interop].
`Window` is an interop type for an arbitrary `JSObject`. There is no [runtime
guarantee] that `Window` is actually a JS [`Window`]. There also is no conflict
with any other interop interface that is defined for the same value. If you want
to check that `Window` is actually a JS `Window`, you can
[check the type of the JS value through interop].

You can also declare your own interop type for the JS types Dart provides by
wrapping them or an interop type they implement with no conflict:
wrapping them:

```dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
Expand All @@ -51,16 +51,16 @@ In most cases, you will likely declare an interop type using `JSObject` as the
don't have an interop type provided by Dart.

Interop types should also generally [implement] their representation type so
that they can be used where the representation type is expected, like in
[`package:web`].
that they can be used where the representation type is expected, like in many
APIs in [`package:web`].

## Interop members

[`external`] interop members provide an idiomatic syntax for JS members. They
allow you to write a Dart type signature for any arguments and return value.
The types that can be used in the signature of these members have
[restrictions]. The JS API they correspond to is determined by a combination of
where they're declared, their name, what kind of Dart member they are, and any
allow you to write a Dart type signature for its arguments and return value. The
types that can be written in the signature of these members have [restrictions].
The JS API the interop member corresponds to is determined by a combination of
where it's declared, its name, what kind of Dart member it is, and any
[renames](#js).

### Top-level interop members
Expand Down Expand Up @@ -92,7 +92,7 @@ exposed in the global scope. To access them, you use top-level interop members.
To get and set `name`, you declare and use an interop getter and setter with the
same name. To use `isNameEmpty`, you declare and call an interop function with
the same name. You can declare top-level interop getters, setters, methods, and
fields, which is the same as a getter and setter pair.
fields. Interop fields are equivalent to getter and setter pairs.

Top-level interop members must be declared with a [`@JS()`](#js) annotation to
distinguish them from other `external` top-level members, like those that can be
Expand Down Expand Up @@ -161,23 +161,20 @@ Within an interop type, you can declare several different types of
`external` interop members:

- **Constructors**. When called, constructors with only positional parameters
construct a new JS object whose constructor is defined by the name of the
create a new JS object whose constructor is defined by the name of the
extension type using `new`. For example, calling `Time(0, 0)` in Dart will
generate a JS invocation that looks like `new Time(0, 0)`. Similarly, calling
`Time.onlyHours(0)` will generate a JS invocation that looks like
`new Time(0)`. Note that the JS invocations of the two constructors follow the
same semantics, regardless of whether they're given a name in Dart or if they
are a factory.
same semantics, regardless of whether they're given a Dart name or if they are
a factory.

- **Object literal constructors**. It is useful sometimes to create a JS
[object literal] that simply contains a number of properties and their
values. In order to do this, you declare a constructor with only named
parameters, as you're creating an object that is defined by the names of
its properties:
parameters, where the names of the parameters will be the property names:

```dart
import ‘dart:js_interop’;
extension type Options._(JSObject o) {
external Options({int a, int b});
external int get a;
Expand All @@ -186,14 +183,13 @@ Within an interop type, you can declare several different types of
```
A call to `Options(a: 0, b: 1)` will result in creating the JS object
`{a: 0, b: 1}`. The object is defined by the invocation, so calling
`Options(a: 0)` would result in `{a: 0}`. You can get or set the properties
of the resulting object through `external` instance members.
`{a: 0, b: 1}`. The object is defined by the invocation arguments, so
calling `Options(a: 0)` would result in `{a: 0}`. You can get or set the
properties of the object through `external` instance members.
:::warning
There's a bug that currently requires object literal constructors to have an
[`@JS`](#js) annotation on the library, empty or otherwise. See [#54801] for
more details.
[`@JS`](#js) annotation on the library. See [#54801] for more details.
:::
- **`static` members**. Like constructors, these members use the name of the
Expand Down Expand Up @@ -286,8 +282,10 @@ one written. For example, if you want to write two `external` APIs that point to
the same JS property, you’d need to write a different name for at least one of
them. Similarly, if you want to define multiple interop types that refer to the
same JS interface, you need to rename at least one of them. Another example is
if the JS name is unusable in Dart e.g. `$a`. In order to do this, you can use
the [`@JS()`] annotation with a constant string value. For example:
if the JS name cannot be written in Dart e.g. `$a`.

In order to do this, you can use the [`@JS()`] annotation with a constant
string value. For example:

```dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
Expand Down Expand Up @@ -387,24 +385,27 @@ occur.
as a JS interop member or type. It is required (with or without a value) for all
top-level members to distinguish them from other `external` top-level members,
but can often be elided on and within interop types and on extension members as
the compiler can tell from the representation type and on-type.
the compiler can tell it is a JS interop type from the representation type and
on-type.

## `dart:js_interop` and `dart:js_interop_unsafe`

[`dart:js_interop`] contains all the necessary members you should need,
including `@JS`, JS types, conversion functions, and various utility functions.
Utility functions include:

- [`globalContext`], which represents the global namespace that the compilers
use to generate JS invocations.
- [`globalContext`], which represents the global scope that the compilers use to
find interop members and types.
- [Helpers to inspect the type of JS values]
- JS operators
- [`dartify`] and [`jsify`], which check the type of certain JS values and
convert them to Dart values and vice versa. Prefer using the specific
conversion when you know the type of the JS value.
- [`importModule`] to import modules dynamically as `JSObject`s.
conversion when you know the type of the JS value, as the extra type-checking
may be expensive.
- [`importModule`], which allows you to import modules dynamically as
`JSObject`s.

More utilities might be added to this library in the future.
More utilities may be added to this library in the future.

[`dart:js_interop_unsafe`] contains members that allow you to look up properties
dynamically. For example:
Expand All @@ -419,22 +420,25 @@ dynamically get, set, and call properties.

:::tip
Avoid using `dart:js_interop_unsafe` if possible. It makes security compliance
more difficult to guarantee and may lead to violations.
more difficult to guarantee and may lead to violations, which is why it can be
"unsafe".
:::

{% comment %}
TODO: Add links when ready

[extension type]: /language/extension-types
[the extension types documentation]: /language/extension-types#type-considerations
[runtime guarantee]: /language/extension-types#type-considerations
[representation type]: /language/extension-types#declaration
[implement]: /language/extension-types#implements
[non-`external` members]: /language/extension-types#members

TODO: Some of these are not available on stable. How do we link to dev?
{% endcomment %}

[global JS scope]: https://developer.mozilla.org/en-US/docs/Glossary/Global_scope
[conversion functions]: /interop/js-interop/js-types#conversions
[declared with a primitive]: /interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs
[contains a primitive type]: /interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs
["JS type"]: /interop/js-interop/js-types
[`Window`]: https://developer.mozilla.org/en-US/docs/Web/API/Window
[check the type of the JS value through interop]: /interop/js-interop/js-types#compatibility-type-checks-and-casts
Expand Down

0 comments on commit 83a613f

Please sign in to comment.