Contract extensibility as it relates to enums and type unions

Say I have a contract returning a type: type CreditCard = { scheme: "Visa" | "Mastercard" } and later we decided to include Amex as card type, then making this change: type CreditCard = { - scheme: "Visa" | "Mastercard" + scheme: "Visa" | "Mastercard" | "Amex" } would be breaking a change. That is, some code like: function processCard(card: CreditCard): number { switch(card.scheme) { case "Visa": return 1; case "Mastercard": return 2; } } would now show a type error. So if I'm designing this API, I figure the only way prevent this is to do one of the following: 1. Preemptively list all card types: type CreditCard = { scheme: "Visa" | "Mastercard" | "Amex" | "DinersClub" | "Discover" } Not a great solution, there could always be a new scheme in the future that we're not aware of. 2. Just make it a string type CreditCard = { scheme: string; } We lose a bunch of type assistance, code complete etc, and now the consumer of our API has to be aware of the behaviour of this API in a way that isn't strictly defined by the spec. 2b. Add a | string catch all type CreditCard = { scheme: "Visa" | "Mastercard" | string; } Basically the same as 2. but at least might provide some type assistance. 3. Accept that adding type union possibilities is always a breaking change.

Jun 19, 2025 - 11:50
 0

Say I have a contract returning a type:

type CreditCard = {
    scheme: "Visa" | "Mastercard"
}

and later we decided to include Amex as card type, then making this change:

type CreditCard = {
-    scheme: "Visa" | "Mastercard"
+    scheme: "Visa" | "Mastercard" | "Amex"
}

would be breaking a change.

That is, some code like:

function processCard(card: CreditCard): number {
    switch(card.scheme) {
       case "Visa": return 1; 
       case "Mastercard": return 2; 
    }
}

would now show a type error.

So if I'm designing this API, I figure the only way prevent this is to do one of the following:

1. Preemptively list all card types:

type CreditCard = {
    scheme: "Visa" | "Mastercard" | "Amex" | "DinersClub" | "Discover"
}

Not a great solution, there could always be a new scheme in the future that we're not aware of.

2. Just make it a string

type CreditCard = {
    scheme: string; 
}

We lose a bunch of type assistance, code complete etc, and now the consumer of our API has to be aware of the behaviour of this API in a way that isn't strictly defined by the spec.

2b. Add a | string catch all

type CreditCard = {
    scheme:  "Visa" | "Mastercard" | string; 
}

Basically the same as 2. but at least might provide some type assistance.

3. Accept that adding type union possibilities is always a breaking change.