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 1.0.0
:
|
|
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 shapes@1.1.0
:
|
|
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 Point
in shapes@1.1.0
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 x
and y
properties in the Point
and they are still there. This function will compile without any troubles even with shapes@1.2.0
. The problem will be if we try to implement the interface in our HappyPainting
application:
|
|
This class works with shapes@1.0.0
but it does not work with shapes@1.2.0
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 HappyPainting
app?
|
|
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 shapes@1.0.0
(and with 1.1.0
and 1.2.0
) but it does not work with shapes@1.3.0
because of the incompatible definition of the distance
method.
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.0.0
to 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?
|
|
So in shapes@1.3.0
there was optional distance
method and in shapes@1.4.0
we removed this method. Now imagine this code in our HappyPainting
app:
|
|
So if there is point.distance
we use it, otherwise, we use the original implementation. This works with shapes@1.3.0
. But using shapes@1.4.0
we get error
Property 'distance' does not exist on type 'Point'
So again, we cannot remove an optional property without causing potential problems.
Solutions
- 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
Point
interface and create a newThreeDimPoint
interface withx
,y
, andz
properties. 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.
Summary
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.