Black Sheep Code
rss_feed

A model of extensible changes.

Published:

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