Strategies for safely renaming your API in Angular

Jeremy Wilken
Clarity Design System
8 min readAug 14, 2018

In Clarity, we have a goal to write our code with the highest quality the first time, but occasionally we have to make the tough decision to deprecate and rewrite certain things from time to time. Sometimes we find that our original implementation doesn’t scale, or more often a new feature requires changing existing behaviors.

We have a strict policy to deprecate in one release and then remove in the next major release, so that anyone using Clarity has time to understand the changes and make any necessary changes. This puts additional burden on Clarity to plan ahead and support deprecated code along with the new implementations side by side.

Here we discuss a few of our strategies for how we manage deprecations, some of which are Angular specific and others are made possible with TypeScript. This blog series looks at several strategies in detail in case you wish to implement the same in your projects, starting with renaming strategies.

If you are building applications, these strategies are just as relevant as they are for library builders. Over time applications have to extend their code base and functionality for new features, and sometimes that means rewriting code. Sometimes it just isn’t possible to rewrite the application in the way you want at a given moment, and you could utilize the same strategies for maintaining your code base.

Renaming your public APIs

We define the public API of your code as any object or property that your application or library exports in a way that any might be used anywhere else besides where it is declared. You might not think your components have an API, but every public property (inputs, outputs, other values) may be used by others. The core goal of deprecating code is to ensure that new code can be released while old code remains for a certain amount of time for consumers to migrate from the old to the new.

We added naming conventions to Clarity that we apply to anything publicly accessible from our library. We wanted to introduce new naming conventions without breaking changes, so we introduced the new names while retaining the original names. For example, we use the prefix Clr on the names of components, like ClrButton so things are obviously part of Clarity, and so our names don’t collide with other libraries. You don’t want to have an application with two libraries trying to provide a Button component in Angular in cases where you might need to import the button using ViewChild. It also means that when you use auto-complete that we don’t pollute the list unless you start typing Clr.

Whatever the reasons you have for renaming, you want to be sure it won’t break other things that might be using it. For example, we know that some projects are using @ViewChild(Wizard) wizard: Wizard; in their projects to get a reference to the wizard, but we wanted to change from the generic name Wizard to ClrWizard. The following steps will show you various examples of how we’ve introduced a whole new nomenclature without breaking changes, with examples of different types.

Renaming a Component, Directive, or Service

When you want to rename a component, directive, or service, there are two ways you might want to handle it. Depending on the type of changes you are making to the component, you can simply create a reference with the correct name without making any functional changes or create a second instance with modified properties.

For the case that you just want to rename the class but not make any other breaking changes, rename the class and then re-export it under the original name as a new value.

// Component or Directive
@Component({ ... })
export class ClrButton {}
/** @deprecated since 1.0 */
export const Button = ClrButton;
// Service
@Injectable()
export class ClrService {}
/** @deprecated since 1.0 */
export const Service = ClrService;

In this example, you create a variable reference to the same object, so that you don’t have two copies of it in your bundles. When you are ready to remove the old version, you simply remove the declaration.

The other case is when you want to rename and make changes to a class. This is more complex due to the way that metadata is processed with Angular. In this case you have to create two instances by extending from the original class and reimplementing the component metadata.

/** @deprecated since 1.0 */
@Component({
selector: 'button',
template: `<button [type]="type"><ng-content></ng-content></button>`
})
export class Button {
protected type = 'button';
}
// New implementation
@Component({
selector: 'clr-button',
template: `<button [type]="type"><ng-content></ng-content></button>`
})
export class ClrButton extends Button {
protected type = 'submit';
}

You construct the new ClrButton class by extending the prototype of Button. Then you can make modifications to the new class, such as overriding values or adding new ones. This makes the new ClrButton implementation unique from the original.

The component decorator and metadata have to be redeclared in this case because you cannot have two full component instances that have the same selector (since they both adopt the same metadata). The Angular compiler has no way to only overwrite one of the component metadata properties, you have to redeclare the whole thing (including the template).

The biggest drawback here is that it becomes increasingly difficult to fully refactor the original implementation out, and you’ll be maintaining two nearly identical implementations.

In either case, make sure you export both instances into your public API so they don’t break other applications using your original API.

Renaming an Interface

Clarity has a number of interfaces that bring consistency and clarity to our API. In the beginning, we didn’t prefix our interfaces properly. To address this, we worked on renaming them for consistency.

Interfaces can be extended, just like classes. Start with naming the existing interface with the desired name. Then re-export it with the old name. In the example below I am also implementing a generic type on the interface.

export interface ClrDatagridComparatorInterface<T> {
compare(a: T, b: T): number;
}
/** @deprecated since 1.0 */
export interface DatagridComparator<T> extends ClrDatagridComparatorInterface<T> {}

You can also change the interface when you extend if you must, but just like with the class example earlier I’d recommend against it if you can avoid it.

Renaming an enum

It isn’t possible to re-export an enum. They are defined once and cannot be modified. As you can see below, when we deprecated some enums in Clarity, we had to redefine the original enum under both names.

export enum ClrDatagridSortOrder {
Unsorted = 0,
Asc = 1,
Desc = -1,
}
/** @deprecated since 1.0 **/
export enum SortOrder {
Unsorted = 0,
Asc = 1,
Desc = -1,
}

That takes care of most of the common TypeScript objects and types. Let’s move on to renaming a few Angular specific examples.

Renaming an Angular Input

Angular inputs have a built in renaming feature, but it is limited and not necessarily what we want. For example, here we can rename the input to clrTitle but the original title will no longer work.

@Input() title: string; // This is the original
@Input('clrTitle') title: string; // This will break anyone using it

Instead, create two inputs that map to the same property. Start by renaming your input to the new name you would like it to be. Then create another input with a setter method that sets the value of the other input.

@Input() clrTitle: string;
/** @deprecated since 1.0 */
@Input()
set title(text: string) {
this.clrTitle = text;
}
get title(): string {
return this.clrTitle;
}

Now your component will support binding to both clrTitle="text" and title="text". Remember, if someone uses both attributes, the last one defined will be used since they are evaluated in order.

Also notice in the example above that we have included a getter on the original title property. This solves the issue for consumers accessing that value in an older version of Clarity. If we did not provide the getter that value would not exist for them and it would be a breaking change. Providing the getter gives us a bridge over breaking changes until the deprecation period is over.

Renaming an Angular output

Angular outputs are a functionally similar to how we renamed an input, except they are essentially read only properties that you can subscribe to and receive emitted events.

First, define the new output like a normal output using an EventEmitter. Then define another output with the original name, and finally have it merge with the other output like you see here.

@Output() clrClickEvents = new EventEmitter<boolean>();
/** @deprecated since 1.0 */
@Output()
get clickEvents(): Observable<boolean> {
return this.clrClickEvents
}

This lets us subscribe to either output (clickEvents)="listen(state)" or (clrClickEvents)="listen(state)",which are equivalent.

Since these are two subscriptions, if you listen to the output stream of both outputs you’ll get the same value twice. This means that it should be clear to consumers to only bind to one event, or they will create two subscriptions instead of one.

Renaming methods and properties

When you have an object with a set of callable properties (methods/functions), you might want to change the names of those properties. We’ve already done this with the Angular input example, but here is a simple way to handle this case to support both names.

Update the existing property to the new name you wish to give it. Then you can use getter and setter methods for the old name and proxy to the new name.

class ClrButton {
clrType = 'button';
/* @deprecated since 1.0 */
set type(value: string) {
this.clrType = value;
}
get type(): string {
return this.clrType;
}
}

If I called ClrButton.type = 'submit'; then it would technically be setting the clrType property value, but still give me the original API without a breaking change. Later, I can safely remove these getter and setter calls without breaking the implementation when I’m ready.

Testing your deprecations

Clarity needs to ensure that deprecated code remains available until it is intentionally removed for the next release. To help facilitate this, we added a unit test file specifically to test deprecations are still available. Essentially we run a set of tests to validate that renamed item matches the original.

// Test class equality
expect(TreeNode).toEqual(ClrTreeNode);
// Can't test interfaces directly, so just verify if they are exported and can be applied.
class ComparatorTest implements Comparator<any> {
compare(a, b) {
return 0;
}
}
expect(new ComparatorTest()).toBeTruthy();
// Test enum values are as expected
expect(SortOrder.Unsorted).toEqual(ClrDatagridSortOrder.Unsorted);

This works well for simple renaming cases, but if you are making modifications then you’ll want to consider how to test both versions of the implementation for the common pieces, and then test the new functionality separately.

We also put a describe block around each version, so we can track the version number when something was deprecated and its expected removal version.

describe('since v0.11, remove in 0.12', () => {
// All tests for things deprecated in 0.11
});

During the next major release cycle, we review this file and ensure we’ve either removed the deprecations that were scheduled or move them to the next release block.

Marking deprecations with comments

One final step that you’ll see in the examples above is the use of comments to mark the deprecated API. This is important for any developer using an IDE or editor that can parse those comments and provide input about how a specific API is deprecated.

To facilitate these features add a comment like the following to the line right above the property or class declaration.

/** @deprecated since VERSION */

The combination of deprecation comments and unit tests helps us manage when things were marked as deprecated and validate they still work as expected.

That wraps up the majority of renaming cases that we’ve run into. If you have additional strategies or cases that I didn’t cover, please mention them and I’ll be happy to expand the content here.

--

--

Jeremy Wilken
Clarity Design System

I talk to my devices. Host of Design for Voice podcast. Google Developer Expert for Assistant and Angular. Work on @VMwareClarity project.