Naming Experimental Features

One of the features I shipped in assistant-ui was file attachments. To give users early access to this feature, I added the experimental_attachments field to our messages object:

type UserMessage = {
  // ...other fields
  experimental_attachments: Attachment[]; // new!
}

A few months passed and I was ready to mark the feature as stable, but renaming experimental_attachments to attachments was a breaking change. The API didn't change, yet users had to update their code.

Prefixes and their downsides

The common approach in JS is to use prefixes like unstable_, experimental_, or dangerous_ to flag APIs that might change.

I care about building a reputation that I take API stability seriously. The experimental_ prefix ensures that users know that this feature is experimental and will not get mad at me if I end up changing the API surface during initial development.

However, I realized that prefixes create a pre-programmed breaking change once these features turn stable.

I wondered if I can do better. I looked at how other programming languages and protocols handle experimental features:

HTTP’s X- Header Convention

Historically, non-standard HTTP headers used the X- prefix, like X-Powered-By. This convention was put into place to avoid name collisions with standard header names.

From RFC 6648:

Historically, designers and implementers of application protocols have often distinguished between standardized and unstandardized parameters by prefixing the names of unstandardized parameters with the string "X-" […] where the "X" is commonly understood to stand for "eXperimental" or "eXtension".

Under this convention, the name of a parameter not only identified the data, but also embedded the status of the parameter into the name itself[…]

However, this practice was deprecated:

The primary problem with the "X-" convention is that unstandardized parameters have a tendency to leak into the protected space of standardized parameters, thus introducing the need for migration from the "X-" name to a standardized name. […]To preserve interoperability, newer implementations simply support the "X-" name forever, which means that the unstandardized name has become a de facto standard (thus obviating the need for segregation of the name space into standardized and unstandardized areas in the first place).

RFC 6648 - Appendix B

It seems like HTTP also ran into the pre-built depreciation problem and decided it’s best to not use a prefix.

NodeJS Feature Flags

NodeJS uses CLI feature flags to enable access to experimental features (e.g. node --experimental-vm-modules index.js). These force the user to explicitly acknowledge and opt-in to the experiments and adjust expectations accordingly. Feature flags also add a lot of friction in the adoption of experimental features, perhaps intentionally.

JSDoc annotations

JSDoc already has support for @deprecated. Marking a feature as deprecated causes it to display strikethrough. This gave me an idea:

type UserMessage = {
  // ...other fields

  /**
   * @deprecated Experimental API might change without notice.
   */
  attachments: Attachment[];
}

This makes the API look as follows to my users:

The developer experience isn’t perfect, the first 2 lines on the popup suggest the API is deprecated (instead of experimental). If only there was an @experimental tag with the same behavior…

https://github.com/microsoft/TypeScript/issues/56808

Being able to ship experimental features is great, because I can get real world feedback before settling on the correct shape of an API. It’s great for my users, who will get early access to a feature and can try it out at their own risk.

I’ve started marking my experimental APIs as @deprecated and it’s been great so far!

Subscribe to my newsletter