2
const add = (x:string|number) => x+x

yields a TS error:

Operator '+' cannot be applied to types 'string | number' and 'string | number'.ts(2365)

But all possible type combination

const add = (x:string) => x+x
const add = (x:number) => x+x

pass type checking as valid. How comes?

Ben Carp
  • 24,214
  • 9
  • 60
  • 72
  • If you have two strings or a string and a number, what is the result of `a+b`? I'd expect it to be a concatenated string, whereas number+number = sum. I'd say that your code is not failsafe, and TS is right on it. – paddotk Aug 11 '21 at 20:10

3 Answers3

2

Presumably you think that adding two string | number operands should succeed and result in a string | number output. Or more generally that the binary + operator should distribute over unions in the types of its operands.

My research indicates the current behavior is as designed, but that it is possibly a design limitation or a missing feature. Some of the numerical operators in TypeScript don't behave exactly as people would like, but the broken or missing use cases aren't critical enough to warrant being addressed (at leas not immediately). In this case, I don't see any evidence that people care enough about adding two string | number operands with + for it to be explicitly supported.

As far as I can tell there are no GitHub issues, open or closed, asking for such support.


Note that it is generally intentional that some valid JavaScript code is considered invalid by TypeScript. See What does "all legal JavaScript is legal TypeScript" mean? for a canonical explanation. That means we have to say something more than "this works in JavaScript" to justify it being allowed in TypeScript. We have to say "this code is likely to be intentional and not a bug".

It is not obvious that adding two string | number together is something people want to do very often; the operations of string concatenation and numeric addition are very different operations that happen to be accessible via +. You can think of + as an overloaded function, and TypeScript does not allow you to call overloaded functions with unions of the parameter types from each call signature; see microsoft/TypeScript#14107 for a feature request to allow such calls.

But it's possible someone could open an issue requesting support for adding two string | number with proper justification about it being intentional more often than it is a bug.


I think such an issue wouldn't do much good though. If we look at microsoft/TypeScript#2031, we see a related issue about why you cannot add two values of type Number (the wrapper object, not the number primitive). At the time this was filed, the TypeScript language specification said that the binary + operator is allowed in an expression a + b if

  • the types of both a and b are assignable to number, in which case the result is a number; or
  • the types of either a or b are assignable to string, in which case the result is a string; or
  • the types of either a or b are any, in which case the result is an any.

None of those allow for Number (or indeed string | number). It is likely that those three possibilities account for the vast majority of usages for binary +, and while it might be nice to support other cases, they are not urgent. Support for things like Number was proposed in microsoft/TypeScript#2361, where it has languished for years without being implemented.

So it clearly isn't critical to anyone on the TS team.


If you want to work around this yourself, you could always do so, like

const add = (a: string | number, b: string | number) =>
  ((a as any) + b) as (string | number);

which would alwas produce string | number, or the following distributive conditional generic thing:

const add = <T extends string | number, U extends string | number>(t: T, u: U) =>
  ((t as any) + u) as (T extends number ? U extends number ? number : string : string);

const five = add(2, 3); // number
const ab = add("a", "b"); // string
const aThree = add("a", 3); // string
const twoB = add(2, "b"); // string
const alsoTwoB = add(Math.random() < 0.5 ? "2" : 2, "b"); // string
const roulette = add(Math.random() < 0.5 ? "2" : 2, 3); // string | number

which produces a narrow output type that depends on the type of each of the inputs.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • after upvoting and accepting your answer, I posted my simplified version which focuses on the `+` abnormality. Is it cool? – Ben Carp Aug 14 '21 at 10:09
  • Sure, although I'm not sure if the point is that my answer is too lengthy and needs to be simplified or summarized. If so, I could possibly add a summary at the top. – jcalz Aug 14 '21 at 20:42
  • I think your answer is excellent, and I hope I can continue reading great answers like that for my future questions :-). However, your covers various aspects for better and worse. Personally for me some of them were less relevant. When I first read it during work time I didn't get it, and I moved on. Only after visiting your resources things fell into place for me. I was not interested in a work around. I am aware that not all legal JS is legal TS. I did not understand "distribute over" (I do now. I think it should have a link instead of unions). – Ben Carp Aug 15 '21 at 03:07
1

Based on Jcalz answer.

To understand this we need to focus our attention at the + operator.

As an operator it doesn't have a clear signature, but Typescript spec does specify which types could go on both sides.

enter image description here

Lets focus on the number and string parameter types. We can think of the plus operator as having the following function overloads:

function plusOperator(left: number, right: number): number
function plusOperator(left: number, right: string): string
function plusOperator(left: string, right: number): string
function plusOperator(left: string, right: string): string

At present Typescript will not allow us to pass string|number to the above pluseOperator function, even though we know that the function can handle it. This is somewhat controversial and a feature request to change this behavior is still open (see issue 14107).

Ben Carp
  • 24,214
  • 9
  • 60
  • 72
0

There is a difference between your first function (with union types) and your other three functions: the other three functions each either always do numeric addition or always do string concatenation. The first function (with union types) sometimes does numeric addition and sometimes does string concatenation.

There are not many situations where you would write a + operator and intend for it to sometimes mean numeric addition and sometimes mean string concatenation, depending on the types at runtime. So code which sometimes does one and sometimes does the other is most probably doing something the programmer didn't intend it to do. It's therefore reasonable for a compiler to label it as an error.

If you know better and you do intend for your + operator to do one or the other depending on the types at runtime, I think that's a justifiable use of a @ts-ignore directive:

function add(a: string | number, b: string | number): string | number {
    // @ts-ignore Intentionally ambiguous use of + operator
    return a + b;
}
kaya3
  • 47,440
  • 4
  • 68
  • 97