Skip to content

Add 'Choose the Appropriate WebIDL Construct for Data and Behavior' #567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jul 2, 2025
Merged
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 137 additions & 1 deletion index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -1533,7 +1533,143 @@ like {{WeakRef}} or {{FinalizationRegistry}},
set accurate author expectations about
the interaction with garbage collection.

<h2 id="api-surface">JavaScript API Surface Concerns</h2>
<h2 id="api-surface">Designing JavaScript APIs</h2>

<h3>Use WebIDL dictionaries, interfaces, and namespaces appropriately</h3>

Use the appropriate WebIDL mechanisms for new APIs.

Web APIs commonly pass around data and functionality using WebIDL.
As a specification editor, decide carefully
between using a dictionary, an interface, or, in rare cases, a namespace.

The goal is to ensure ergonomic, consistent APIs that feel natural to Web developers while avoiding pitfalls like "fake classes" or classes that provide no functionality.

Each construct has its own pros and cons, discussed below.

<h4 class="no-num no-toc">Use Dictionaries for “Configuration” or “Input-Only” Data</h4>

Choose a dictionary when the part of the API represents data that is transient or you need a configuration-style object or "options bag".

Dictionaries are ideal for when the data doesn't get stored or mutated; it's just used it at the time of the call.

For example, the `ShareData` member from Web Share:

```WebIDL
dictionary ShareData {
USVString title;
USVString text;
USVString url;
};
```

And how it's commonly used:

<pre class="highlight">
const data = { "text": "Text being shared" };
await navigator.share(data);
</pre>

Dictionaries are easily extensible and makes it easy to add optional fields later as needed.
Members of a dictionary are optional by default, but can be marked as `required` if needed.

Dictionaries are also highly idiomatic (i.e., natural to use in JavaScript). Passing `{ ... }` inline is the most natural way to supply configuration in JavaScript.

Dictionaries, because of how they are treated by user agents, are also relatively future-proof; accommodating new members gracefully, without breaking older code.

Dictionaries are best used for objects that don't need to be distinguished by type in their lifecycle (i.e., `instanceof` checks are mostly meaningless because it's always `Object`).

A key thing to know about dictionaries is that they are "passed by value" to methods (i.e., they are copied) and that browsers engines strip unknown members when converting from JavaScript objects to a WebIDL representation.
This means that if a developer changing the value after it is passed into an API has no effect.

Again, taking the `ShareData` dictionary as an example:

```JS
const data = {
"text": "Text being shared",
// Ignored by a browser that does not include a "whatever" parameter.
"whatever": 123,
};
navigator.share(data);

// Changing this after calling .share() has no effect
data.text = "New text";
```

<h4 class="no-num no-toc">Choose an Interface for Functionality, State, and Identity</h4>

Intefaces are roughly equivalent to classes in JavaScript.
Use an interface when a specification needs to bundle state--
both visible properties and internal "slots"--
with operations on that state (i.e., methods).

Unlike dictrionaries, interfaces:

* provide the ability to check object's identity (i.e., one can check if it is an `instanceof` a particular class on the global scope),
* can have need read-only properties,
* can have state,
* can exhibit side-effects on assignment.

Defining an interface also exposes it on the global scope, allowing for the specification of static methods. For example, the `canParse()` static method of the URL interface.

```JS
if (URL.canParse(someURL)) {
// Do stuff...
}
```

If warranted by the use cases, give the interface a constructor.
Do not add a constructor if a class has no state.
Doing so is considered *bad practice*, as it is effectively creating a "fake class": that is, a class that doesn't do anything useful on its own, doesn't hold state, etc.
(see `DOMParser` or `DOMImplementation` as bad examples).

In such cases, prefer a static method on an existing object or, if absolutely necessary, mint a new namespace.

<h4 class="no-num no-toc">When an interface provides useful data, add a serializer</h4>

If the interface holds data that is useful when serialized (e.g., `GeolocationPosition`), consider adding a `toJSON()` default method. Alternatively, if it holds binary data, one can add .toBlob() an so on.

This makes object natural to use with APIs. For example:

```JS
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});

// Prepare the fetch request options
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// .stringify() calls .toJSON() automatically
position
};
```

<h4 class="no-num no-toc">Choose a namespace to Avoid “Fake Classes” for Behavior-Only Utilities</h4>

A namespace is correct if everything is purely static and lacks a prototype (like the `Math`, `Intl`, `Atomics`, `Console` objects in JS).

If you only have one or two small static functions, a whole new namespace may be overkill. Attaching them to an existing object might be more idiomatic.

Conversely, a namespace that grows too large or too broad may need better organization or separate logical partitions.

<h4 class="no-num no-toc">"Pseudo-namespaces"</h4>

As WebIDL interfaces can have attributes, it is possible to create "pseudo-namespaces" by leveraging non-contructable interface as attributes.

A common example are all the attributes attaches to the navigator object. The navigator object has an interface definition (`Navigator`), which iteself contains other
atttributes that gives access to further functionality through interface instances.

For example:

* `navigator.credentials` - The `CredentialsContainer` interface of the Credentials Management API.
* `navigator.geolocation` - The `Geolocation` interface of the Geolocation specification.
* `navigator.permissions` - The `Permissions` interface of the Permissions specification.

Psudo-namespaces are useful for when, as a spec author, you need access to "this" particular instance
(e.g., where the properties of one instance may different from those of another browsing context).

<h3 id="attributes-like-data">Attributes should behave like data properties</h3>

Expand Down
Loading