A model of extensible changes.
Extensible changes, as opposed to breaking changes are a strategy we can use to continue development on some code, without being disruptive to existing code that relies on it.
We can add new functionality, new features, without requiring existing users of the code to make any updates. If users of our code are required to update their code in response to our changes, those are considered breaking changes.
When requiring data (eg. request bodies, function parameters)
🟢 You can always:
Add new optional properties
type Foo = {
name: string;
value: string;
+ nickName? : string;
}
Reasoning: Existing code can continue not passing this extra property in.
Widen existing properties
type Foo = {
name: string;
- value: string;
+ value: string | number;
}
Reasoning: Existing code can continue passing just strings in.
Make existing mandatory properties optional.
type Foo = {
name: string;
- value: string;
+ value?: string;
}
Reasoning: Existing code can continue always passing this property in.
Remove mandatory or optional properties
type Foo = {
name: string;
- value: string;
}
Reasoning: Existing code can continue passing this property in. The presence of the extra property should not break things in the function.
☝️ In TypesScript there's a bit of nuance with this answer. You may need to use the suppressExcessPropertyErrors
flag.
See: Example 1 - no flag
Example 2 - suppressExcessPropertyErrors
flag
🛑 You can not:
Add new mandatory properties
type Foo = {
name: string;
value: string;
+ color: string;
}
Code that would break with this change:
doSomethingWithFoo({name: "foo", value: "bar"}); // Code did not pass a color property in
Make optional properties mandatory
type Foo = {
name: string;
value: string;
- nickName?: string;
+ nickName: string;
}
Code that would break with this change:
doSomethingWithFoo({name: "foo", value: "bar"}); // Code did not pass a now mandatory nickName in
Narrow existing properties
type Foo = {
name: string;
- value: string | number;
+ value: string;
}
Code that would break with this change:
doSomethingWithFoo({name: "foo", value: 1}); // Code passed a now disallowed number value in
When returning data (eg. response bodies, function return values)
🟢 You can always:
Add additional optional or mandatory properties
type Foo = {
name: string;
value: string;
+ color: string;
+ age?: number;
}
Reasoning: The presence of extra properties should not break the code that receives this data. (See caveat below).
Make optional properties now mandatory
type Foo = {
name: string;
value: string;
- nickName?: string;
+ nickName: string;
}
Reasoning: If the calling code was handling the presence of this property, it will continue to do so. If it was not handling the presence of this property, it's presence shouldn't break anything.
Narrow existing properties
type Foo = {
name: string;
- value: string | number;
+ value: string;
}
Reasoning: The calling code will already be handling values of both type.
Remove optional properties
type Foo = {
name: string;
- nickName?: string;
}
Reasoning: The calling code will already be handling the scenario where this property does not exist.
🛑 You can not:
Remove mandatory properties
type Foo = {
name: string;
- value: string;
}
Code that would break with this change:
const myFoo : Foo = getFoo();
foo.value.split(''); // Value not longer exists to do .split on
Widen existing properties
type Foo = {
name: string;
- value: string;
+ value: string | number;
}
const myFoo : Foo = getFoo();
foo.value.split(''); // .split doesn't exist on a number type
Make mandatory properties optional
type Foo = {
name: string;
- value: string;
+ value?: string;
}
Code that would break with this change:
const myFoo : Foo = getFoo();
foo.value.split(''); // value may not exist to .split on
⚠️ A caveat
This model of extensibility assumes that consumers of your API understand this model of extensibility, and specifically the 'returned data can contain additional properties at anytime' part.
An example of some code that would be broken by this model of extensibility:
// The API code
type UserEnrichmentData = {
favouriteColor: string;
favouriteAnimal: string;
}
function getSomeData() : UserEnrichmentData {
// returns some data
}
// The consumer code
const user = {
name: "foo",
userType: "student"
};
const userEnrichmentData = getSomeData();
const enrichedUser = {
...user,
...userEnrichmentData
}
assert(enrichedUser.name === "foo");
The potential problem here is what if we change UserEnrichmentData like:
type UserEnrichmentData = {
favouriteColor: string;
favouriteAnimal: string;
+ name: string;
}
Now the name
property that comes from getSomeData()
will clobber the existing name
property, which is likely not intended behaviour.
To resolve, consumers of the API should either selectively pick values off the return value, or always do the spread of the return value first.
-const userEnrichmentData = getSomeData();
+const {favouriteColor, favouriteAnimal} = getSomeData();
const enrichedUser = {
...user,
- ...userEnrichmentData
+ favouriteColor,
+ favouriteAnimal
}
or:
const userEnrichmentData = getSomeData();
const enrichedUser = {
- ...user,
- ...userEnrichmentData
+ ...userEnrichmentData
+ ...user,
}
Questions? Comments? Criticisms? Get in the comments! 👇
Spotted an error? Edit this page with Github