W3C

Web API Design Cookbook

W3C Working Group Note 02 October 2012

This version:
http://www.w3.org/TR/2012/NOTE-api-design-20121002/
Latest published version:
http://www.w3.org/TR/api-design/
Latest editor's draft:
http://darobin.github.com/api-design-cookbook/
Editors:
Robin Berjon, Robineko
송정기(Jungkee Song), Samsung Electronics Co., Ltd.

Abstract

This document captures common practices in designing APIs that fit well into the Web platform as a whole, using WebIDL [WEBIDL].

Status of This Document

This section describes the status of this document at the time of its publication. Other documents may supersede this document. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at http://www.w3.org/TR/.

This document has been developed progressively over time with help from the WebApps WG and the ScriptLib CG.

This document was published by the Device APIs Working Group as a First Public Working Group Note. If you wish to make comments regarding this document, please send them to [email protected] (subscribe, archives). All feedback is welcome.

Publication as a Working Group Note does not imply endorsement by the W3C Membership. This is a draft document and may be updated, replaced or obsoleted by other documents at any time. It is inappropriate to cite this document as other than work in progress.

This document was produced by a group operating under the 5 February 2004 W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent which the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

Table of Contents

1. Introduction

Over a relatively short period of time the number of different APIs being created for use on the Web has grown at a sustained pace. In working on these interfaces, many in the community discuss the benefits of certain approaches over others and reach agreement as to the preferred solution when facing a given problem.

Keeping track of all these gems is however difficult given the volume of work being carried on in parallel and the sometimes disjointed nature of the groups involved. As a result, it can take a long while and many arguments repeated almost identically before a good solution becomes common.

What's more, it is altogether too rare to garner feedback from developers who are actually using these features in their day-to-day work. Because of this, several APIs have shipped with issues that could have been caught earlier on in the process. This document therefore also endeavours to elicit feedback from developers in order to make it more readily available to API designers.

The goal of this document is to capture such ideas as they appear and accrete them over time so as to constitute a repository of knowledge on this topic. As a guide it does not however attempt to supplant editors' brains in making decisions as to how to design their APIs, and consequently one must keep in mind that the reasoning behind a specific recommendation is often more important than its result. Furthermore, in some cases there may not be a single consensual best approach, and editors will need to understand the tradeoffs involved in order to pick what works for them amongst a set of options.

2. Basics

This section covers the very basics of API creation for the Web platform. If you are familiar with the APIs found there, you might wish to simply skim the titles.

2.1 Casing

Casing is relatively coherent across the platform, and if you are familiar with JavaScript development it should come to you rather naturally.

Interfaces, dictionaries, exceptions, and callbacks all use “CamelCase”. That is to say no underscores are used and the first letter of every word is in uppercase. The rules when one of those words is an acronym are not necessarily well established — follow your instinct (or try to avoid acronyms).

Example 1
FascinatingDocument
Node
SomethingBadHappened
XMLHttpRequest

Both attributes and methods use “pascalCase” also known as “interCaps”. Which is to say that the first letter is lowercase, no underscores are used, and all the following logical words in the name start with an uppercase letter.

Example 2
obj.thisIsAnAttribute
obj.simple
obj.doSomethingNow()
obj.run()

Constants are all uppercase, with logical words separated by underscores. Families of constants tend to have common suffixes. Please note that constants are decreasingly used and that it is likely you will not be having recourse to this construct very much (see Don't Use Numerical Constants).

Example 3
REALLY_STUPID_ERROR
PASTA_SAUCE_TYPE

Events are a bit special and for historical reasons have somewhat more convoluted rules. The interface that represents a specific event will follow the general rules for interfaces, ending with Event. But the event name will be all lowercase and with no underscores. Likewise, the on* event attribute will be the event name (all lowercase and glued together) simply prefixed with “on”.

Example 4
SomethingHappenedEvent
obj.addEventListener("somethinghappened", handler, false);
el.onsomethinghappened = handler;

2.2 Using attributes

It is not always obvious when one should use an attribute rather than a method. The basic rule is simple: use attributes as often as possible.

Never use getFoo()/setFoo() accessor pairs unless it is very clear that these are operations that have strong effects beyond just changing the property of the object. If you wish your interface to also be usable in a language other than JavaScript and for which such accessors tend to be needed, the bindings for that language can naturally generate the accessor pair. Don't do this:

Example 5: Bad usage
var len = obj.getLength();
arr.setLength(num);

But rather:

Example 6
var len = obj.length;
arr.length = num;

Beyond operations that obviously require a method (that take parameters, return a computed value, carry out an action, etc.) there are specific cases in which what might at first seem like a candidate for being an attribute is best handled as a method:

Modifies the value
In some cases there may be relations between objects such that when one is set as the value of another it needs to be updated to reflect its end of the relationship as well. For instance, when setting objA.child = objB it may be necessary to set objB.parent to objA behind the scenes. Such side effects call for a method: objA.setChild(objB).
Asynchronous access
In some instances, an object may be available but it is best if some of its fields are only loaded lazily because they require time to compute. The canonical example is the size of a file. If processing objects representing files, making the size of each file available immediately will be costly, and acquiring it synchronously upon attribute access would lead to poor performance (those stat calls are not cheap). In such cases it is best to make the accessor an asynchronous method that is passed a callback such as someFile.size(function (bytes) { }) (or, if there are many such properties on the object, have a single call to load them all, for instance someFile.stat(function (info) { })).

2.3 When To Be Asynchronous

The Web platform's runtime model is based on a single-threaded event loop. Because of that, any time a method requires even a little bit of time to run, it blocks the thread, and therefore the execution of anything else on the page — leading to bad user experience.

Example of operations that can take a while to complete include reading from the network, accessing the disk, performing a database search, or waking a sensor up.

Another case in which asynchronous operations are required is for all security entry points. Some operations (e.g. obtaining the device's physical location) can require the user agent to obtain the user's permission. If that were to be done synchronously, the entire application would freeze while the user makes her decision. This is undesirable in terms of user experience, but it also a bad security model. The user needs to have all the time she wants in order to make such decisions and should not feel pressured by a frozen application (people make bad security decisions when they want to get something done).

It can seem tempting to make methods either synchronous or asynchronous at the developer's discretion. The simple answer to that is: don't. The result can be catastrophic: for instance, while in development a method that has to interact with a remote resource may return quickly because of light load on the server and proximity between the developer (who is using a powerful device on a fast network) and the server, so that the synchronous call may seem fine. But once deployed, it can cause long, very perceivable freezes for the end-users. The XMLHttpRequest interface does make that option available on its open() method and there is broad consensus that it was a bad decision that should never be emulated.

A context that is not similarly affected by synchronous issues is that of Web Workers (since they run in a separate conceptual thread from the Web application's user interface). In such cases, it can sometimes be useful to provide a synchronous variant of an asynchronous interface that would be available solely in a Worker context. This is primarily done in order to make development simpler in such contexts and should not be considered required.

2.4 Don't Use Numerical Constants

Interfaces regularly require constant identifiers. You have a node and you want to know if it's an element or a comment. You have a message and you want to know if it's email or SMS. That much makes sense. What does not make sense is insisting on naming these things with numbers when we could name them with, you know, names.

There are several variants in this bad practice. First, is the lovingly terse approach:

Example 7
if (foo.type === 17) doSomething();

Then there is the comment you have to paste every time:

Example 8
// check if this of the fungible type
if (foo.type === 17) doSomething();

Or we can have the uselessly long variant:

Example 9
if (foo.type === FooInterface.FUNGIBLE_TYPE) doSomething();

And there's the meh option:

Example 10
var isFungible = FooInterface.FUNGIBLE_TYPE;
// ...
if (foo.type === isFungible) doSomething();

None of the above is as simple, terse, and self-documenting as:

Example 11
if (foo.type === "fungible") doSomething();

We're not in the 1970s, standardising UNIX system calls. Strings are lightweight enough, and readable. There is no need to define string constants to support these: just the plain strings, defined as such in specification prose, work. In some languages, this can lose static verification, but in JavaScript it makes no difference: if you misspell the the string you might have equally misspelt the constant, with the same failure.

Additionally, interfaces that don't start out by listing a dozen (useless) constants are more readable. The only downside to doing away with numerical constants is the negative impact it may on occasion have on feature detection. It would however be better to rely on other aspects that enable feature detection, since these constants can easily be added for a half-baked feature (and frequently have been).

2.5 Namespacing and Prefixing

If you are developing functionality for use inside of a Web runtime (but not a browser) that is likely to both share code with existing Web projects (typically, libraries like jQuery) and that also plans to evolve and integrate improvements to the Web platform on a regular basis, then you should make sure that the interfaces and methods you add cannot conflict with ones added to the core platform.

For methods and attributes that extend existing interfaces, you should rely on vendor prefixing, i.e. simply prefixing your additions with a short string that is your company's or project's name.

Example 12
xhr.acmePassiveFTP();
document.acmeDigitalSignature;

For interfaces that you add, rather than prefixing all of their names which can get tedious, it can be simpler to just make them available under your own namespaced object.

Example 13
var toaster = new acme.Toaster();

3. WebIDL Constructs

WebIDL [WEBIDL] is a powerful and flexible schema language for APIs. It is a useful foundation in ensuring that APIs can be defined interoperably without each and everyone of them needing to repeat all manners of behavioural specifics.

It can however be somewhat daunting to use at times, and the intricacies of the platform mean that it has some dark or at least non-obvious corners. This chapter endeavours to help clarify at least enough of it that you will feel comfortable making use of it.

3.1 Using Dictionaries

While designing Web APIs, you might encounter situations where you need to define an interface that has no method. That is, for certain requirements, you need a method to define a property bag object that would be expressed as an Object object or as an object literal in JavaScript implementation. This is where the dictionaries come into play.

This section presents a guideline to use dictionaries at the right place in your specification without any common mistake. First, in the "When to use" section, we show the typical cases you can make use of dictionaries. Second, in the "How to use" section, we provide a list of features that you can check in order not to make a common mistake with dictionary usage.

3.1.1 When to use

Property bag objects are commonly used to define optional parameters for certain methods of given interfaces. Also, they can be used to define necessary data structures and data formats in a given application context. Here are the representative examples:

Case 1: Arguments to Interface Method When designing interfaces using WebIDL, you can use dictionaries to define a property bag object that carries multiple arguments to its method. In many practices, the methods take dictionary value as an optional argument although it is not mandatory.

Example 14
// WebIDL
partial interface IDBDatabaseSync {
    IDBObjectStoreSync createObjectStore (DOMString name, 
                                          optional IDBObjectStoreParameters optionalParameters);
};

dictionary IDBObjectStoreParameters {
    DOMString? keyPath = null;
    boolean autoIncrement = false;
};

// JavaScript
var parameters = {
    keyPath: "id"
,   autoIncrement: true
};
trans.db.createObjectStore("Contact", parameters);

More examples in published specifications: http://www.w3.org/TR/FileAPI/#creating-revoking

Case 2: Arguments to Constructor Using dictionaries, you can also define a constructor arguments to the given interfaces. This allows the developers create and pass an object or an object literal to the constructor of the JavaScript objects.

Example 15
// WebIDL
[Constructor(IntentParameters params),
 Constructor(DOMString action, DOMString type, optional any data, optional
 sequence<Transferable> transferList)]
interface Intent {
    readonly attribute DOMString     action;
    readonly attribute DOMString     type;
    readonly attribute any           data;
    readonly attribute MessagePort[] ports;
    readonly attribute any           extras;
    void postResult (any data, optional sequence<Transferable> transferable);
    void postFailure (any data);
};

dictionary IntentParameters {
    DOMString              action;
    DOMString              type;
    any                    data;
    sequence<Transferable> transfer;
    Object                 extras;
    URL                    service;
    sequence<URL>          suggestions;
};

// JavaScript
var intent = new Intent({
                          action:    "http://intents.w3.org/pick",
                          type:      "image/png",
                          extras:    { multiple: true, count: 5 },
                          service:   "http://example.com/pick-image"
                      });

More examples in published specifications: http://www.w3.org/TR/2012/WD-dom-20120405/#interface-event or http://www.w3.org/TR/FileAPI/#blob

Case 3: User object A dictionary is normatively defined as an associative array data type with a fixed, ordered set of key-value pairs. That is, you can use a dictionary to define a custom data type carrying a set of properties as an object format used in the given application context.

Example 16
// WebIDL
partial dictionary Media {
    MediaContent   content = {};
    DOMString      title;
    DOMString      description;
    DOMString      type = "*.*";
    DOMString     author;
    DOMString     path;
    float         size;
    sequence<DOMString>   tags;
    Date          publishDate;
    Resolution    resolution;
};

// JavaScript
var mediaSequence = [];
var mediaObject = {
    content: { uri: "http://example.com/images/kitten.png" },
    title:        "",
    description:  "",
    type:         "image/png",
    author:       "",
    path:         "/local/path/to/png/kitten.png",
    size:         1000000,
    tags:         ["tom and jerry", "miyau"],
    publishedDate:  "",
    resolution: { width: 1920, height: 1080 }
};
mediaSequence.push(mediaObject);
intent.postResult(mediaSequence);

More examples in published specifications: http://www.w3.org/TR/2012/WD-gallery-20120712/#the-media-dictionary or http://www.w3.org/TR/2012/WD-contacts-api-20120712/#the-contact-dictionary

3.1.2 How to use

3.1.2.1 Use sequence type

Dictionaries are always passed by value. Hence, when you define a dictionary member that should carry an array of certain value, use sequence type rather than Array type. Sequences are always passed by value while arrays are passed by reference.

Example 17
dictionary IntentParameters {
    DOMString              action;
    DOMString              type;
    any                    data;
    sequence<Transferable> transfer;
    Object                 extras;
    URL                    service;
    sequence<URL>         suggestions;
};

In addition, there is another check point where sequence type should be used rather than array type. WebIDL describes that the dictionary type must not be used as a type of element of an array. Hence, when your dictionary has to carry an array of the dictionary values as its member, use sequence type.

Example 18
dictionary DoNotUseLikeThis {
    IntentParameters[] params;
};

dictionary UseLikeThis {
    sequence<IntentParameters> params;
};
3.1.2.2 Inheritance

Dictionaries support inheritance as interface does. A dictionary must not be defined having a cycle in its hierarchy.

Example 19
dictionary Novel : Book {
    // dictionary-members...
};

dictionary Book : Novel {
    // dictionary-members...
};
3.1.2.3 Optional Dictionary Member and Defaulting Values

On a given dictionary value, the presence of each dictionary member is optional. Therefore, in order to indicate that certain members are required fields in your specification, use default values. Dictionary members with default values are always considered to be present.

Example 20
dictionary PropertyBag {
    DOMString iAmRequired = "default";
    DOMString iAmOptional;
};

In the example, since iAmRequired has default value, it is always considered as present when the dictionary value is passed. On the other hand, when iAmOptional is missing, it is regarded as absent.

3.1.2.4 Attribute, Constant, or Exception fields

It is one of common mistakes that a dictionary type is used with attribute field in interface definition. In face, dictionaries must not be used as the type of attribute, constant or exception fields.

Example 21
interface HasWrongUsage {
    readonly attribute IAmDictionary dict;
};

To correct it, use type any or object and describe in prose that the attribute takes the type of the dictionary.

Example 22
interface HasRightUsage {
    readonly attribute any dict;
};

In your spec, write "dict is of type IAmDictionary dictionary as defined in [REFERENCE]".

3.1.2.5 No Nullable Dictionary

Do not use dictionary type as inner type of a nullable type. By definition in WebIDL dictionary, valued with null, is regarded to be an empty dictionary, which is not null. (It can even have the value of members defined in their default values.)

Example 23
dictionary HasWrongUsage {
    IAmDictionary? dict;
};

dictionary HasRightUsage {
    IAmDictionary dict;
};

As members of a dictionary are optional by definition, the developer who reads your specification can omit the member that she intends to be not present in the given application context.

3.1.2.6 Partial Dictionary

As with interfaces, the IDL for dictionaries can be split into multiple parts by using partial dictionary definitions.

Example 24
dictionary SomeDictionary {
    // dictionary-members…
};

partial dictionary SomeDictionary {
    // dictionary-members…
};
3.1.2.7 Extended attributes

Only the following two extended attributes are applicable to dictionary members: [Clamp], [EnforceRange].

3.2 Interfaces

Interfaces are the core work horse of WebIDL as they are the means through which functionality is exposed. They are introduced with the interface keyword, have a name, and a body that contains their definition in terms of constants, attributes, and methods (known as “operations” in WebIDL). A typical interface looks like this:

Example 25
interface BeerBottle {
    // body goes here
};

Without modifiers, an interface will become available in the global context as an object with the same name.

Interfaces can inherit from other interfaces, in which case they acquire their properties. Inheritance is specified like this:

Example 26
interface BeerBottle : Bottle {
    // body
};
interface Spork : Spoon, Fork {
    // body
};

As in the last example above, an interface can inherit from any number of other interfaces. This practice however is discouraged if it can be avoided as it entails more complex resolution rules and can be difficult to implement in some contexts. Multiple inheritance is often a sign that the API design should be revisited and can be simplified.

3.2.1 Partial Interfaces

It is common to have to add functionality to an existing interface, or to wish to define a single interface spread over multiple locations. This can happen primarily for two reasons:

  • The interface may be defined in another specification and extended by a new one. This is a relatively common pattern for core interfaces off which things may be attached (e.g. Navigator). It's an important aspect of the Web platform's core extensibility.
  • Some editors occasionally consider it useful to split the definition of an interface into smaller pieces as part of the same document. This is sometimes done to make the specification clearer (though the advantages here are debatable), or sometimes because part of the interface is optional.

In both cases there is a need to extend an existing interface, and inheritance is not the solution since one effectively wishes to mix functionality into the existing interface, and not create a new one.

A construct that was previously used for this but is no longer recommended was to create the extending interface with [NoInterfaceObject] and then state that the existing interface implements the extending one. This is exemplified below:

Example 27
// we wish to add findUnicorns() to Navigator
[NoInterfaceObject]
interface UnicornNavigator {
    Unicorns findUnicorns (DOMString name);
};
Navigator implements UnicornNavigator;
// you can now call navigator.findUnicorns("tab");

This is clunky however, and no longer necessary. It can still be found in some drafts but should not be copied.

The proper way of implementing this feature is to use partial interfaces. These are just like regular interfaces, except that they are defined to specify simply a fragment of the whole, and can therefore add to existing interfaces that may be defined anywhere else.

The previous example can therefore best be rewritten as:

Example 28
partial interface Navigator {
    Unicorns findUnicorns (DOMString name);
};

3.2.2 [Constructor] and [NamedConstructor]

If you have read WebIDL as found in many specifications, you will have noticed that there are special constructs that can decorate existing ones in order to enhance them. These are called “extended attributes” and their goal is not to change the WebIDL itself — which is to say that they don't modify the abstract schema described by WebIDL — but to influence how the concrete language bindings operate.

In this section we look at several extended attributes that have a bearing on how interfaces are bound in JavaScript.

By default, when an interface is bound in JavaScript, an interface object for it is available globally but it is not constructible. That is to say that it can be used for instance for instanceof testing, or that it can expose some members such as static methods, static attributes, or constants but you can't construct it with new. Try out for instance in your JavaScript console:

Example 29
new Node()
// TypeError: Node is not a constructor

Therefore, in order to use a constructor in JavaScript, you will need to use the [Constructor] extended attribute. [Constructor] can optionally take parameters (not giving it parentheses is the same as giving it an empty set) and you can specify multiple ones.

An example might make this clearer:

Example 30
// new Unicorn();
[Constructor]       // the same as [Constructor()]
interface Unicorn {
    // ...
};

// new Unicorn();
// new Unicorn("tab");
// new Unicorn(5, 82);
[Constructor,
 Constructor(DOMString name),
 Constructor(unsigned long wingspan, unsigned long age)]
interface Unicorn {
    // ...
};

At times it is desirable to have the constructor for an interface have a different name from that of the interface. For such cases, use the [NamedConstructor] extended attribute. It works in the exact same way as [Constructor], except that it also accepts an identifier that specifies the name to use for the constructor. An example will show the syntax:

Example 31
// new Rose();
// new Rose("pink");
[NamedConstructor=Rose,
 NamedConstructor=Rose(DOMString colour)]
interface RosoideaeRosa {
    // ...
};

3.2.3 [ArrayClass]

Another useful extended attribute for interfaces is [ArrayClass]. It comes in handy when it is necessary to define an interface that can behave just like a real JavaScript array. It is meant to be used with the getter, setter, creator, and deleter keywords for indexed properties.

These keywords are rather straightforward:

  • The getter defines what happens when a value is fetched from the array, so that for instance the following can work: var carrots = shoppingList[7];.
  • The setter and creator is the reverse and defines what happens when a value is set in the array (on an existing entry or a newly minted one — for arrays you usually want to use the same operation for setter and creator), for instance with the following: shoppingList[0] = "beer"; or shoppingList.push("unicorns");
  • Finally, the deleter defines what happens when a value is removed from the array, for instance with: shoppingList.pop();.

Assembling this all together looks like this:

Example 32
[ArrayClass]
interface ShoppingList {
    attribute unsigned long length;
    getter object getShoppingItem (unsigned long index);
    // note how both creator and setter are used here
    creator setter object setShoppingItem (unsigned long index, object item);
    deleter void removeShoppingItem (unsigned long index);
};

The above give you a ShoppingList interface. You could use it as follows:

Example 33
var list = obj.findShoppingListFor("robin");
list = list.concat("beer tea basil ham cat-food".split(" "));
list.forEach(function (item) {
    remindUser("Shop for " + item);
});
list.pop(); // bad cat
console.log("Getting " + list.join("\n"));

3.2.4 When to use [NoInterfaceObject]

There have been many excessive uses of [NoInterfaceObject] — unless you are absolutely certain that it is what you want, you should avoid it. The odds are good that what you are looking for is either a partial interface or a dictionary.

In practice it should only be used for abstract interfaces that enrich a concrete one through the implements statement.

3.3 Constants

Constants are a simple construct that does exactly what it say on the tin. It creates a member of a given type on an interface (or an exception) the value of which cannot be changed, introduced with the const keyword.

Constants are limited in the values they can accept. They can take numerical values (including the likes of Infinity and NaN), booleans, or null (for cases in which you might forget what the value of null is). Examples tell you all you need to know about the syntax:

Example 34
interface Something {
    const short ANSWER = 42;
    const boolean WRONG = false;
    const unsigned long BIG_NUMBER = Infinity;
};

While they were previously used rather commonly, there are decreasing reasons to want to resort to constants on interfaces. Before using them, be sure to be aware of Enums and Don't Use Numerical Constants.

3.4 Enums

An enumeration is used to specify a set of strings that are acceptable, it is typically used to define options that are restricted to a limited vocabulary.

Enumerations are defined with the enum keyword, a type name, and then a list of strings (with no duplicates) inside curly brackets. The name can then be used where type identifiers usually appear.

Example 35
enum Unit {
    "m",
    "cm",
    "mm"
};
interface Unicorn {
    void setHornSize (unsigned long length, Unit unit);
};
// This works
//    unicorn.setHornSize(42, "cm");
// This blows up as well it should
//    unicorn.setHornSize(7, "in");

When an attribute is defined to be an enumeration type, then assigning a value to it that is not in the enumerated set simply does nothing.

3.5 Attributes

Attributes are a fundamental building block of interfaces. They are used to specify data fields of a given type. An attribute is introduced with the attribute keyword.

They can be read-only in which case they cannot be assigned to, or read-write. It is important to note that if the value of a read-only attribute is not atomic but has attributes of its own, those are not made read-only as well — they keep operating just as specified by that type. An example:

Example 36
interface Name {
    attribute DOMString givenName;
    attribute DOMString familyName;
};
interface Person {
    readonly attribute Name fullName;
};

In the above definition, it is impossible to assign a new value of the fullName field of Person. So you could not do the following: billie.name = someName;. But the fields of the Name object itself stay just as re-write so that they can still be set, as in billie.name.familyName = "The Cat";.

An attribute can also be said to be static, in which case it is an attribute of the interface rather than being associated with the object. Static attributes are rarely good ideas and can tend to indicate a bad design.

Example 37
// set the debug flag globally
interface Runtime {
    static attribute boolean debug;
};

A specific behaviour can be added to attributes to define how a given object is turned into a string, using the stringifier modifier. When a object is cast to a string, if a stringifier has been specified (there can be only one) then the string produced will be the value of that attribute. Naturally, that attribute has to be of type DOMString.

Example 38
// another Person interface
[Constructor(DOMString name, short age)]
interface Unicorn {
    stringifier attribute DOMString fullName;
    attribute short age;
};

This can then be used with the following code:

Example 39
var uni = new Unicorn("Tab", 42);
document.getElementById("output-unicorn").textContent = uni;
// that element now has content "Tab";

3.6 Methods

Methods are the workhorses of WebIDL. In the specification they are known as “operations”, but since everyone calls them methods we have decided to stick to this term as it will be more readily understood. They are used to define behaviour.

Methods have a return type, and a list of arguments which can be precisely specified. As for attributes, a method can also be said to be static, in which case they become available on the interface object directly.

Example 40
interface KittenFactory {
    static Kitten makeMeANewKitten (DOMString name);
};
// var billie = KittenFactory.makeMeANewKitten("Billie The Cat");

The return type can be any type or void, which indicates that nothing is returned.

The most complex part of a method's definition is its arguments list. It can contain any number of argument definitions, each of which has a type and a name; all of them separated by commas.

Note that it used to be that arguments were all preceded by the in keyword, which was a leftover from OMG IDL. Since Web specifications make no use of the out and inout argument styles of the aptly named OMG specification, the in keyword has been dropped and must not be used.

A basic method therefore looks like this:

Example 41
interface Unicorn {
    void gallop (unsigned long speed, Pattern rhythm);
    AudioStream speak ();
};

The arguments can be modified in a number of ways. First, an argument can be marked as optional in which case it can be omitted when the method is invoked. If so, it should be noted that all the arguments that are positioned after it in the list will also be considered optional.

Example 42
enum Direction { "left", "right" };
interface Dahut {
    void turnAround (optional Direction whichWay, short degrees);
};
// I can call someDahut.turnAround()

Note that in the above it is not indicated that the degrees argument is optional even though it implicitly is. For readability purposes, it is best to always indicate that an argument is optional even when it implicitly is as someone scanning the method definition quickly from the end might not notice it.

A problem with the above method definition is also that while my dahut can only turn left or right, there is no indication of what happens if I omit the argument. Is it right or left? Or random? This can be remedied in specification prose but it is better to specify a default directly in the WebIDL, as follows:

Example 43
enum Direction { "left", "right" };
interface Dahut {
    void turnAround (optional Direction whichWay = "left", short degrees);
};
// when I call someDahut.turnAround(), it now turns left

An argument can also be variadic. This indicates that that argument can be repeated zero or more times when the method is called. This can only be done if it is the last argument in the arguments list. Variadic arguments are specified by appending "..." to the type.

Example 44
interface Restaurant {
    DeferredFood orderFood (DOMString... dishName);
};
// I can now call
//  resto.orderFood("spam", "spam", "spam", "spam", "spam", "spam", "spam", "spam", "spam");

3.7 Overloading and Union Types

WebIDL makes it possible to use overloading of methods in interfaces so that there can be more than one method with a given name that takes a different set of arguments. This happens simply by specifying the method more than once.

Note that method overloading is rarely needed and should usually be avoided unless there is no better solution, if only because the restrictions on how it can be used and the resolution rules can be quite complex. A lot of the cases in which one may think that overloading may be required are actually better addressed with optional arguments, variadic arguments, or at times union types.

Union types are used where types can be used, but instead of specifying just one type they indicated that the values can be of several different types. They are specified between parentheses, with each type separated by the or keyword.

Example 45
interface Party {
    void bookDate ((Date or unsigned long or DOMString) date);
};
// I can now call with...
//  a JS Date object
//    party.bookDate(new Date(…));
//  time since epoch
//    party.bookDate(1337998907987);
//  a magic string
//    party.bookDate("tomorrow");

This can be useful for return values as well:

Example 46
interface Cryptozoology {
    (Dahut or Unicorn) findAnimal ();
};

3.8 Exceptions

Exceptions are special classes that can be used to be thrown. They are very limited interfaces that can only define constants, and a limited kind of attribute (known as “fields”) that are defined solely by a type and a name.

All exceptions automatically get a message and a name field (for which the specification should provide values), such that those need not be explicitly specified.

It is strongly recommended that new specifications do not define new exceptions, but rather reuse DOMException while providing a specific name for it.

In addition to DOMException, a number of predefined exceptions are also available that can be reused by specifications. This list is: Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError and URIError (these correspond to their JavaScript equivalents as defined in [ECMA-262]).

Due to these constraints on usage, we have decided not to delve deeply into exceptions in this section.

3.9 Callbacks

It is very common for asynchronous methods in Web APIs to accept callbacks, which are functions that can be called with specific arguments when called back. These are specified with the callback keyword, a name, a return type, and an arguments list which is similar to that used for a method.

Specifying callbacks is simple:

Example 47
// defining callbacks
callback LandingOk = void (unsigned long landingSpeed);
callback LandingFail = void (DOMString gravity, short casualties);

// using callbacks
interface FlyingUnicorn {
    void land (LandingOk, LandingFail);
};

// in code
myFlyingUnicorn.land(
        function (speed) {
            console.log("Successfully landed at " + speed + "kph!");
        }
    ,   function (severity, dead) {
            console.log("Crashed landing. Damage level: " + severity + ". Dead elves: " + dead);
        }
);

3.10 Sequences and Arrays

Issue 1

I admit to still being largely mystified by this. Recent discussion on the mailing list does not at all dispel that feeling.

3.11 WebIDL Legacy Features

The features of WebIDL listed in this section have been specified for the sole purpose of describing legacy APIs such as are found in “DOM 0”. It is particularly important that you do not use them unless you are trying to document one such API — they must not be used for any kind of new work. The list includes:

Issue 2

Get an actual list.

4. Common Patterns

Beyond understanding the basics of WebIDL there are common and useful idiomatic constructs that can usefully be shared. This section endeavours to progressively accrue some more, so please send yours in!

4.1 Ensuring that Feature Detection is Possible

Issue 3

We had talked of Yehuda writing this section. I will see if I can hunt him down.

4.2 Specifying Callbacks

Asynchronous method calls require a point of reentry in the form of a callback. There are multiple ways of specifying these, none of which is consensually consider the one and only approach.

One is success/error callback functions, as used in [GEOLOCATION-API]. It takes the shape of a method call that accepts a callback function for success followed by one for errors. There may be arguments before the success callback, and the error callback is usually optional.

Example 48
navigator.petKitten(
    function() {
        console.log("kitten is purring");
    },
    function(err) {
        console.log("kitten hates you, because: " + err);
    }
);

Downsides of this approach are that it is a little more verbose and does not encourage error handling. It also lacks in extensibility since adding new arguments to the method will typically require placing them after the optional error handler — making them de facto optional. Likewise, extending it to support not just callbacks for success and error but also for other situations that may warrant reporting information back (e.g. progress being made) is clumsy at best. Since it is only used in [GEOLOCATION-API] it does not appear to be a required pattern for consistency.

Another approach is to define new DOM Events that are called when an underlying system changes.

Example 49
window.addEventListener("kittenpurr", function() {
    console.log("kitten is purring");
}, false);

On one hand, most developers are quite familiar with DOM Events, and they make it easy to have several handlers for a given event.

On the other hand, using DOM Events for listening to systems that need specific activation (e.g. powering up) are usually not a good idea: addEventListener is supposed to be free from side effects, and associating activation code with it breaks that promise. The [DEVICE-ORIENTATION] specification is as such an anti-pattern that should not be replicated. See some discussions on side effects in addEventListener.

Another is returning an EventTarget object, as pioneered by [XMLHTTPREQUEST]. This consists in opening a request that returns an object on which event handlers can be registered, then starting that request with a specific call.

Example 50
var kitpet = navigator.openKittenPetting();

kitpet.onpurr = function () {
    console.log("kitten is purring");
};

kitpet.ondrool = function () {
    console.log("kitten is now drooling");
};

kitpet.onviciousbite = function () {
    console.log("kitten bit your hand off");
};

kitpet.start();

The approach is the most verbose and does not encourage error handling, but it does have the advantages of being trivially extended for greater feedback, and of being familiar to users of [XMLHTTPREQUEST] (and now [INDEXEDDB]). It can, however, feel rather overkill if extensions for additional events does not seem likely.

Then there are NodeJS style callbacks. For every asynchronous operation, every callback takes as first parameter an error object which is null when the operation was a success, followed by as many pieces of information as are needed to capture the success.

Example 51
navigator.petKitten(function (err, state) {
    if (err) console.log("kitten mauled your face with extreme prejudice");
    else console.log("kittem manifest joy by " + state);
});

The primary value in this approach is that it puts errors "right there" where they are likely to be handled. Its consistency is also very helpful in creating chains of callbacks, a feature that proves precious when multiple callbacks can become nested. The major downside of this approach is that it is currently not used in the rest of stack and therefore probably only familiar to NodeJS developers (which granted represent a fast-growing proportion of the general web developer population).

Finally there are promises. These are similar to returning an EventTarget object but more generic and use a slightly different style.

Example 52
var promise = navigator.petKitten();
promise.then(function () {
    console.log("kitten is purring");
}).fail(function () {
    console.log("kitten bit your hand off");
}).run();

There is elegance and flexibility in promises, but to date they are used in no Web specification.

4.3 Specifying Events

Issue 4

This ought to be relatively straightforward.

Issue 5

Should we have a section on specifying with Web Intents?

A. Other Sources

A number of other sources provide information that can be useful in writing specifications. This list is by no means complete, suggestions are welcome.

Anne maintains a document called How To Spec on how to specify certain features of the platform. Some parts of it are tailored to the specific markup that is used when processing specifications with Anolis, but the general advice is sound.

B. Acknowledgements

Many thanks to the following people, in no particular order: Cameron McCormack, Marcos Càceres, Andreas Gal, Rick Waldron.

C. References

C.1 Normative references

[WEBIDL]
Cameron McCormack. Web IDL. 27 September 2011. W3C Working Draft. (Work in progress.) URL: http://www.w3.org/TR/2011/WD-WebIDL-20110927/

C.2 Informative references

[DEVICE-ORIENTATION]
Steve Block, Andrei Popescu. DeviceOrientation Event Specification. 1 December 2011. Last Call Working Draft. (Work in progress.) URL: http://www.w3.org/TR/2011/WD-orientation-event-20111201/
[ECMA-262]
ECMAScript Language Specification. June 2011. URL: http://www.ecma-international.org/publications/standards/Ecma-262.htm
[GEOLOCATION-API]
Andrei Popescu. Geolocation API Specification. 22 December 2008. W3C Working Draft. (Work in progress.) URL: http://www.w3.org/TR/2008/WD-geolocation-API-20081222/
[INDEXEDDB]
Nikunj Mehta, Jonas Sicking, Eliot Graff, Andrei Popescu, Jeremy Orlow. Indexed Database API. April 2011. Working Draft. (Work in progress.) URL: http://www.w3.org/TR/IndexedDB/
[XMLHTTPREQUEST]
Anne van Kesteren. The XMLHttpRequest Object. 15 April 2008. W3C Working Draft. (Work in progress.) URL: http://www.w3.org/TR/2008/WD-XMLHttpRequest-20080415