Suppose we have a Typescript library called
shapes exporting an interface called
Point. What change can we make to this interface so we can release a new version of
shapes as a minor change, not breaking anyone’s code?
This is not an easy question as it sounds like. I’ve witnessed many people releasing a new version of a library as a minor change without realizing that this change is going to break someone else’s code.
Let’s look at our interface
Point in our
shapes library that currently has version
It just represents a point in the 2D plane.
Removing a property from the interface
Let start with a simple case. What happens when we remove a property from the interface? Suppose we no longer want to work with the 2D plane, one dimension is good enough for us thus we want to remove the
y: number property and we release it in
Did we break something? Well, yes. Imagine another application, let’s call it
HappyPainting, that uses our library and contains this function:
Suddenly the interface
email@example.com does not contain
point.y property and we get a compilation error.
Property 'y' does not exist on type 'Point'
So remember: removing a property from an interface is a breaking change.
Adding a new property to the interface
Let’s add a new field to the interface: the
z coordinate as we’re slowly moving to three dimensions:
Will it break anything? It definitely does not break the
distanceFromOrigin function as the function needs just
y properties in the
Point and they are still there. This function will compile without any troubles even with
firstname.lastname@example.org. The problem will be if we try to implement the interface in our
This class works with
email@example.com but it does not work with
firstname.lastname@example.org because the class does not define the
z coordinate. We get this error:
Property 'z' is missing in type 'PointIn2D' but required in type 'Point'.
We broke someone’s code by adding a new property to the interface.
Adding a new optional property
If we cannot add a new required property maybe we can add a new optional property, right? Let’s stick with a 2D point and add a new method called
distance. It supposes to compute the distance from the point to the given point:
We made the method
distance optional so there should be no problem with it, right? The function
distanceFromOrigin has no problem with this change because it doesn’t use the method
distance. And the class
PointIn2D has no problem with it because the method
distance is optional.
But what if we had this class in the
Maybe someone had a similar idea – implementing a
distance method but with a different definition. Instead of a Point, it takes two numbers as arguments. The class
Another2DPoint works with
email@example.com (and with
1.2.0) but it does not work with
firstname.lastname@example.org because of the incompatible definition of the
Property 'distance' in type 'Another2DPoint' is not assignable to the same property in base type 'Point'. Type '(x: number, y: number) => number' is not assignable to type '(point: Point) => number'.
If we upgrade from
1.3.0 the app won’t work.
Removing an optional property
Ok, what about removing an optional property? Let’s remove our optional
distance method. Can we hurt someone?
email@example.com there was optional
distance method and in
firstname.lastname@example.org we removed this method. Now imagine this code in our
So if there is
point.distance we use it, otherwise, we use the original implementation. This works with
email@example.com. But using
firstname.lastname@example.org we get error
Property 'distance' does not exist on type 'Point'
So again, we cannot remove an optional property without causing potential problems.
- The first obvious solution is to release the library as a new major version. This is the least we can do. We should mention it in the readme or changelog and we are out of the woods. The downside of this solution is that we moved the burden to the client. The new version won’t be installed on the applications depending on
"shapes": "^1.0.0". Someone has to probably manually update package.json, read the readme, and apply the necessary changes.
- Use a different interface. Keep the old
Pointinterface and create a new
zproperties. Everyone using the old interface is going to be fine and if someone wants to use the third dimension, they have to update the code anyway.
- Don’t export the interfaces at all. Let the client decide what it wants and provide just implementations. It’s more work for the client in the beginning but you can freely develop the library without the instant worries about breaking the code because some interface was changed.
- Have small interfaces. This is a good idea in general. Usually, it’s better to have more smaller interfaces than a few big interfaces.
Changing a public interface is a tricky thing. You never know how is the interface used in the outside world and in general, better be safe than sorry. Of course, if you develop just internal libraries and you can verify every module using your library, the situation is slightly better for you.
Some of the errors may depend on how strict you set your typescript compiler. But you should always expect the worst.