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!