Skip to main content

Versioning Internal Changes

Symbol-Keyed Internal State recommended exporting the internal tracking symbol under an /** @internal */ tag rather than hiding it completely, specifically so a test suite (see Testing Symbol-Keyed Internal State) can inspect it directly. That design choice has a consequence worth calling out on its own: an exported symbol, no matter how clearly it's marked @internal, is still importable by anyone, not just your own test suite.

What Counts as a Breaking Change Here​

For a mixin's genuinely public API — the methods and types documented as part of its interface, like createForm or WithFormMixin — the usual rules apply: renaming or removing one of those is a breaking change, full stop.

Internal, symbol-keyed state is murkier. In principle, nothing about it is part of the supported contract, and a determined consumer who imports the internal symbol and starts poking at it is going outside what the library promises. In practice, the symbol is sitting right there in the package's exports, discoverable by anyone reading the source or the type definitions — and once something is technically importable, someone eventually will import it, whether or not it was meant for them.

Given that, treat a rename or restructuring of @internal-marked state as a change worth a changelog entry and at least a minor version bump, even though it doesn't touch the mixin's documented public surface. This isn't the same obligation as a true breaking change to createForm's signature, but it's not nothing either — silently renaming managedForms to something else in a patch release could break someone's test suite (or worse, some other internal tooling built on top of yours) with no warning.

Making the Boundary Explicit​

The /** @internal */ JSDoc tag is a convention, not an enforcement mechanism — nothing stops TypeScript or a bundler from treating an @internal-tagged export exactly like any other export. Two things make the boundary clearer for consumers reading your code:

  • State it in the README or CONTRIBUTING docs, not just in a code comment: "Exports tagged @internal are not part of this library's stable API and may change in any release without being treated as a breaking change to the public interface." This sets expectations before anyone goes looking at the source.
  • Keep the internal symbol's name descriptive of what it's for, not of how it's currently implemented — managedForms describes its purpose (the forms this mixin manages) rather than its shape (formsArray, say). This makes it more likely the name itself survives an internal refactor (switching from an array to a Map, for instance) even if what it points to changes underneath.

Summary​

Public API changes and internal state changes aren't governed by the same rules, but they aren't unrelated either. A mixin's CONTRIBUTING or README should say plainly which parts of its exports fall into which category, so "is this a breaking change?" has a documented answer instead of being a judgment call made fresh every time something internal shifts.