8

In terms of deprecation strategies, we can mark a value as deprecated and this can imply that 'this thing exists now and should work, but in future major releases this thing may disappear and will break your code'.

eg:

type MyClient =  {

    /** 
      @deprecated - prefer using `bar` instead 
    */
    foo: () => string; 
    bar: () => Promise<string>; 
}

At the same time, in terms of an extensible/non-breaking strategy - we can always allow additional optional inputs to add extra functionality without breaking existing code:


type MyClient =  {

    /** 
      @deprecated - prefer using `bar` instead 
    */
    foo: () => string; 
-    bar: () => Promise<string>; 
+    bar: (options?: {x: boolean}) => Promise<string>; 

}

In this scenario, I want to indicate that that options object will be mandatory in future major releases - is there a term for this kind of annotation?

2
  • 1
    Write it as an overload, and deprecate the no-args form (e.g. tsplay.dev/weOodm, tsplay.dev/WGgj0W - not sure if you can do it with a type).
    – jonrsharpe
    Commented Aug 7 at 6:53
  • Yeah, it's called "breaking changes". If you make mandatory something that was previously optional, you are going to break people's code. It's like having two overloads of the same function and then removing one. Not sure if there is an annotation specifically used to express that (and also, if you can add such an annotation, you can make it required to begin with), but it's a good idea to warn people. What would be better, generally speaking, is to provide both versions for a while, mark the optional one deprecated, and phase it out over time. Commented Aug 7 at 21:05

2 Answers 2

9

That's still a deprecation. You will be deprecating the optional nature of the parameter, which will be a breaking change.

For the purpose of your consumer being alerted to the deprecation, it's irrelevant whether you're going to be removing bar entirely or changing bar in a way that old usages will break.

It's a deprecation, and it should be treated as a deprecation, because that's what it is: a currently valid way of working today will break in a future version.

It is of course relevant to also document what the new way of working is/will be; but whether that's a totally different way of working or a similarly named method with a slightly different signature is an irrelevant distinction. We don't spend time quantifying the degree to which it is a breaking change. Either it's a breaking change or it's not. "A bit of a breaking change" is not a thing.

7
  • Indeed; I would say that "the use of X is deprecated in programs that don't do Y". This concept is especially relevant in situations where supporting X when not needed may have some disadvantages, but in some cases the cost of working around a lack of X in would exceed the cost of supporting X. Having implementations shift toward supporting X only when needed would improve efficiency, but that doesn't imply that X itself should be deprecated.
    – supercat
    Commented Aug 7 at 15:03
  • Depending on the language there could be several degrees of "breaking changes". They all a "breaking" of course, but it can be source-compatible, for example, so you'll only need to recompile instead of changing your code. Or it might be "breaking" (changing) the behavior of function in some corner case that won't "break" 99% of the consumers.
    – Dan M.
    Commented Aug 7 at 17:01
  • @DanM. Requiring any action from a consumer is a breaking change. If I omit a field from a particular HTTP response, a field which the consumer doesn't use but does indirectly deserialize it via the package I provided for them, their code will still break. Even if all they have to do it get my latest package (which omits that property), recompile, and redeploy, I still broke their original application. The kind of change you have to make is irrelevant in terms of defining a breaking change. The necessity of the consumer needing to act is what defines a breaking change.
    – Flater
    Commented Aug 7 at 22:52
  • @Flater that's what I wrote. Please reread my comment. The point is - not all actions require changes from all consumers and there is also a degree of the required actions, hence there is a "degree of breaking changes". Something can indeed be "a bit of breaking change" from some perspective.
    – Dan M.
    Commented Aug 8 at 14:07
  • @DanM.: I vehemently disagree. Library/API designers should not be looking at what specific consumers may or may not be doing and then deciding whether their change may or may not be breaking. It's way more effort and it blows back on you when you're wrong. The breaking nature of a change depends on the change alone, not on whether Alice and or Bob might actually be using that feature today or not. If you remove any existing behavior from your library/API, no matter who may or may not be using it, for the purpose of your library/API versioning it is a breaking change.
    – Flater
    Commented Aug 8 at 23:44
2

As @jonrsharpe points out in the comments, this is simply overloading a function and deprecating one of the overloads.

In TypeScript you would do it this way:


async function bar(options: {x : boolean}) : Promise<string>; 
/**
 * @deprecated - options object will be mandatory
 */
async function bar() : Promise<string>; 
async function bar(options?: {x: boolean}) : Promise<string> {
    return ""; 
}

type MyClient = {
    bar: typeof bar, 
}

async function main(client: MyClient) {

    const result1 : string = await client.bar(); 
    const result2 : string = await client.bar({x: true}); 
}

(Straight declaration of a union of function signatures doesn't play nicely in TypeScript - see: https://stackoverflow.com/questions/43662729/union-type-of-two-functions-in-typescript)

Note the IDE deprecation strikethrough hint:

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.