Typescript and Array.reduce()

Was toying with the following code in typescript.

type Summable = string[] | number[];

let a: Summable = ['a', 'b'];
let b: Summable = [1, 2];

let addUp = function (arg: Summable) {
    return arg.reduce((a, c) => a + c);
}

The idea was that addUp() would take in an array of numbers or strings and return the sum (if the array was composed of number) or the concatenation (if the array was composed of strings)

Typescript doesnt seem to like Array.reduce method.

This expression is not callable.
Each member of the union type ‘{ (callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string) => string): string; (callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string) => string, initialValue: string): string; (callbackfn: (previousValue: U, currentValue: string, …’ has signatures, but none of those signatures are compatible with each other.ts(2349)

I find this confusing…

At first I took it to mean that.reduce() passed arguments of different types to the callback. However, TS doesnt seem to have any problem with .map() which passes similar arguments.

Any insight would be appreciated.

1 Like

I think it’s a bug. See here.

@m3g4p0p : I’d be interested to hear your take.

2 Likes

Array.reduce doesn’t work · Issue #44063 · microsoft/TypeScript · GitHub

Issue’s still open and being investigated as of a month ago… something to do with a ‘known limitation’ of reduce in particular…

I don’t think that’s specific to reduce()… if you write it out manually you’ll get a friendlier error message:

function addUp (arg: Summable) {
    let [head, ...tail] = arg

    for (const current of tail) {
        head += current
    }

    return head
}

Operator ‘+=’ cannot be applied to types ‘string | number’ and ‘string | number’.

IMU this is because the union type is not an exclusive “one of” type, but anything that matches either of its members; so string[] | number[] would basically be equivalent to (string | number)[]. Also consider the following, which is perfectly valid:

interface IHuman {
    name: string
    age: number
}

interface ITable {
    legs: number
}

const humanTable: IHuman | ITable = { 
    name: 'Fred', 
    legs: 5 
}

But then I would have expected that something like following would do the trick:

type Summable <T> = T extends string 
  ? string[] 
  : T extends number 
  ? number[] 
  : never

So AFAIC there might be a bug after all. :-P

1 Like

So, you’re saying that because the union type (Summable) can be either an array of strings or an array of numbers, inside the addUp function, TS has no way of knowing which of these it is dealing with.

Did I get that right?

Hm, the error message suggests that the union would also allow mixed arrays of the given types… it turns out that my understanding was wrong though:

type Summable = string[] | number[]
const mixed: Summable = [42, 'foo']

Type '(string | number)[]' is not assignable to type 'Summable'. Type '(string | number)[]' is not assignable to type 'string[]'. Type 'string | number' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'.

Yeah, right. This was what was confusing me :slight_smile:

I got it working with a type guard (source), but that’s an ugly solution:

type Summable = string[] | number[];

function isString(arg: any): arg is string {
  return typeof arg === 'string';
}

function isStringArray(arg: any): arg is string[] {
  return Array.isArray(arg) && arg.every(isString);
}

function addUp(input: Summable) {
  if (isStringArray(input)) {
    return input.reduce((acc, val) => acc + val);
  }

  return input.reduce((acc, val) => acc + val);
}

I was also attempting to implement a conditional type when you posted this:

So this means if type T is assignable to type string then the type of Summable is string[], otherwise , if the type of T is assignable to type number then the type of Summable is number[]. Finally, we use never in the final else branch to make the case that T is assignable to neither string or number an error:

Did I get that right?

Like you, I would have expected that to work, but:

type Summable<T> = T extends string
  ? string[]
  : T extends number
  ? number[]
  : never;

const a: Summable<string> = ['a', 'b']; // OK
const b: Summable<number> = [1, 2];  // OK

function addUp(input: Summable<string | number>) {
  return input.reduce((acc, val) => acc + val); // Error. This expression is not callable
}

Which leads me to think bug.

And one final question (sorry), apart from the “not callable” error in that final example, TS underlines acc and val in my editor and gives the following warning:

Parameter 'acc' implicitly has an 'any' type, but a better type may be inferred from usage.
Parameter 'val' implicitly has an 'any' type, but a better type may be inferred from usage.

How would one handle that?

1 Like

Ah, funnily enough I had tried a generic type guard to just check if all elements are of the same type… but this wouldn’t solve the issue:

function addUp(input: Summable) {
  const [head, ...tail] = input
  
  if (tail.some(el => typeof el !== typeof head)) {
    throw new TypeError()
  }

  return input.reduce((acc, val) => acc + val)
}

… maybe to abstract or something. oO

Yes that was the idea… FWIW I also found a neat suggestion for a generic XOR type, but the issue remained the same for typed arrays. So yeah, I concur it might be a bug.

Not sure, but probably related to the same bug?

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.