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. reponse 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 precense of this property, it will continue to do so. If it was not handling the precense of this property, it's precense 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",
type: "student"
},
const userEnrichmentData = getSomeData();
const enrichedUser = {
...user,
...userEnrichmentData
}
assert(enrichedUser.name === "foo");
The potential problem here is what if we change UserEnrichementData 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,
}