Home > Software engineering >  Why can I call array.some() but not array.every() with union array types?
Why can I call array.some() but not array.every() with union array types?

Time:11-10

So, imagine I have a (dumb) function like this:

function doSomething(input: number|string): boolean {
  if (input === 42 || input === '42') {
    return true;
  } else {
    return false;
  }
}

Why am I allowed to call array.some() like this:

function doSomethingWithArray(input: number[]|string[]): boolean {
  return input.some(i => doSomething(i));
}

But not array.every() like this:

function doEverythingWithArray(input: number[]|string[]): boolean {
  return input.every(i => doSomething(i));
}

Which gives me this error:

This expression is not callable. Each member of the union type '{ (predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; } | { ...; }' has signatures, but none of those signatures are compatible with each other.

I don't understand the difference. In my mind either both should work, or neither. What am I missing?

Note that doSomething() accepts number|string as its argument, so it should work with every element of number[]|array[], like it does for array.some(), shouldn't it?

CodePudding user response:

That has to do with the definition of every()

every<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): this is readonly S[];

In a case of your union the signature become's

{
  <S extends number>(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): true; 
  (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): this is S[]; 
} |
{ 
   <S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): true;
    (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): this is S[];
}

You can see that the predicates have incompatible types.

CodePudding user response:

As we already discussed in the comments, every has an overload containing a generic function with a type predicate while some has no such things.

Why are the function signatures "incompatible" now?

  • Its definitely related to the fact that they are generic. I have yet to find a definitive answer. Maybe @jcalz knows a satisfying explanation :)

Why is every generic and has a type predicate?

  • Contrary to some, calling every can have an effect on the type of the array. If you have an array like

    const arr: (string | number)[] = []
    

    Calling every on it and checking if every element is a number should also change the type of the resulting array.

    const arr2 = arr.every((e): e is number => typeof e === "number")
    // arr2 should be number[]
    

    This is only possible to achieve with type predicates. And to allow type predicates to have an effect here, every must be generic. (Otherwise the type predicate would be ignored by every)


So what can you do about it?

I would suggest changing the type of input to an intersection of (number[] | string[]) and (number | string)[]. This may look weird but it does fix the error here.

function doEverythingWithArray(
  input: (number[] | string[]) & (number | string)[]
): boolean {
  return input.every(i => doSomething(i));
}

The function also remains callable as before.

// works as before
doEverythingWithArray(["a", "b", "c"])
doEverythingWithArray(["a", "b", "c"] as string[])
doEverythingWithArray([0, 1, 2])
doEverythingWithArray([0, 1, 2] as number[])

// fails
doEverythingWithArray([0, 1, "a"]) 

Playground

  • Related