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, 
}