Section 4 · Tooling and Delivery
Deprecation Paths
18 min3 min readPut in order
Lesson at a glance
Remove tokens, props and components without breaking teams, by shipping the replacement first, deprecating loudly in every channel, automating migration with codemods, and deleting only in a major release.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Removing things is the hardest part of running a design system. Adding a component makes one team happy; removing one makes forty teams do work they didn’t plan for. Northwind’s first major release removed eleven props and six tokens in one go, with a migration guide published the same day. Two teams were still on the old major nine months later, quietly forking fixes. The removals themselves were right. The path to them was missing.
The deprecation path
Every removal at Northwind now follows the same steps, spread over several releases.
1. Ship the replacement first. You can’t ask teams to migrate to something that doesn’t exist. Button’s variant prop shipped in 4.3.0 as a plain addition, a minor release.
2. Deprecate in every channel at once. In the same release, kind is marked deprecated everywhere people might meet it:
type ButtonProps = {
/** @deprecated Use `variant`. Removed in 5.0.0. */
kind?: 'primary' | 'secondary' | 'danger';
variant?: 'primary' | 'secondary' | 'danger';
// …rest of the props
};
const warned = new Set<string>();
function warnOnce(key: string, message: string) {
if (process.env.NODE_ENV === 'production' || warned.has(key)) return;
warned.add(key);
console.warn(`[Northwind UI] ${message}`);
}
export function Button({ kind, variant, ...rest }: ButtonProps) {
if (kind) warnOnce('button-kind', 'Button `kind` is deprecated; use `variant`. Removed in 5.0.0.');
const resolvedVariant = variant ?? kind ?? 'secondary';
// …render as before, using resolvedVariant
}The @deprecated JSDoc tag makes editors strike the prop through as people type. The warning appears once per session, only in development builds; bundlers replace process.env.NODE_ENV with a constant so the check and message drop out of production code. The docs page gets a “Deprecated” badge, and the Figma component property is renamed so designers notice too.
Tokens get the same treatment. The old name stays as an alias of the new one, so nothing breaks, and the token file says so:
{
"color": {
"text": {
"light": {
"$type": "color",
"$value": "{color.text.subtle}",
"$deprecated": "Use color.text.subtle. Removed in 5.0.0."
}
}
}
}3. Automate the migration. A codemod is a script that rewrites source code. For a renamed prop, a jscodeshift transform is a few lines:
export default function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source)
.find(j.JSXOpeningElement, { name: { name: 'Button' } })
.find(j.JSXAttribute, { name: { name: 'kind' } })
.forEach((path) => {
path.node.name.name = 'variant';
})
.toSource();
}Teams run it with npx jscodeshift --parser=tsx --extensions=tsx,ts -t codemods/button-kind.js src/ (without those two flags jscodeshift only reads .js files with the Babel parser), then review the diff. For the five largest consumers, Northwind’s core team opens the migration pull requests itself. A rename that would cost forty teams an afternoon each becomes forty quick reviews.
Codemods have limits, and the migration guide should name them. The transform above renames kind="danger" and kind={isUrgent ? 'danger' : 'primary'} correctly, because both are attributes written on a <Button>. It can’t see kind hidden inside spread props, such as <Button {...actionProps} /> where actionProps was built in another file, or a Button re-exported under another name. That’s why the runtime warning matters even after the codemod has run: it catches the cases static rewriting can’t reach. Northwind’s guide for every removal has the same four parts: before-and-after code, the codemod command, the known cases the codemod misses, and a link to the office hours slot where the core team helps with the rest.
4. Measure, then remove. Code search across repositories tells you how many uses remain; Section 5 covers how Northwind collects that automatically. When usage is near zero, the removal goes into the next major release, batched with other planned removals so teams face one upgrade instead of several.
5. Support the previous major. After 5.0.0 ships, 4.x still gets bug and security fixes for six months. Teams that can’t upgrade immediately aren’t stranded, and they don’t fork.
Announce it like a product change
A deprecation is news for forty teams, so treat it like a product announcement rather than a line in a changelog. Northwind posts every deprecation in the design system channel with three sentences: what is changing, why it’s worth the effort, and when the removal happens. The post links the migration guide and is pinned until the major ships. Engineering managers get a monthly summary of upcoming removals that affect their teams, because they’re the ones who schedule the work.
How long is long enough?
Northwind keeps a deprecated API for at least two minor releases or three months, whichever is longer, before the major that removes it, and publishes the major release schedule a quarter ahead. Your numbers can differ; what matters is that they’re written down and predictable, so teams can plan migrations into their own roadmaps instead of discovering them in a failed build.
In the exercise, you put a full deprecation in order. Next, you build the release pipeline that catches unintended breaking changes before they ship.
Key takeaways
- Never deprecate something until its replacement has shipped; a deprecation without an alternative is just a complaint.
- Mark deprecations everywhere people look - types, tokens, dev-only console warnings, docs and Figma - and always name the replacement and the removal version.
- Codemods turn a migration from a task for 40 teams into a review for 40 teams.
- Remove deprecated things only in a major release, after measuring that usage is near zero, and keep patching the previous major for a stated period.