How come I am able to use array.some() but not array.every() when working with union array types?

Imagine a scenario where there is a (silly) function like this:

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

Question arises as to why array.some() can be called like this:

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

However, calling array.every() in the same manner results in an error:

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

This generates the following error message:

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.

The difference between the two scenarios is puzzling. Logically, both should either work or fail. What could potentially be overlooked?

Note that doSomething() accepts number|string as its argument, hence it should operate with every element of number[]|array[], similar to how it functions with array.some(), right?

Answer №1

This is a current limitation within TypeScript; reference microsoft/TypeScript#44373 for more information.


In the past, TypeScript struggled with invoking methods on a combination of array types. Initially, calling unions-of-functions was not allowed due to the challenge of merging multiple signatures into one coherent function. Check out microsoft/TypeScript#7294:

// Before TS3.3:
declare const arr: string[] | number[];
arr.some(() => true); // error
arr.map(() => 1); // error
arr.every(() => true); // error

With TypeScript 3.3, some progress was made through microsoft/TypeScript#29011, by accepting an intersection of parameters from individual union members.

However, this enhancement only covered basic scenarios where no more than one member of the union was generic or overloaded. If the merged array methods were either generic or overloaded, they remained uncallable. Thus, there was an advancement but limitations persisted, like with map(). See microsoft/TypeScript#36390:

// TS3.3
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // error
arr.every(() => true); // okay

Subsequently, a generic overload was introduced to every() in order to serve as a type guard function; view microsoft/TypeScript#38200. Although helpful, this modification hindered every() functionality on unions of array types with the release of TypeScript 4.0:

// TS 4.0
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // error
arr.every(() => true); // error

For TypeScript 4.2+, microsoft/TypeScript#31023 addressed limited support for calling unions of generic call signatures provided the generic type parameters matched. However, challenges persist with overloaded call signatures:

// TS 4.2+
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // okay
arr.every(() => true); // error 

That's our current standing.


Perhaps future updates will address microsoft/TypeScript#44373. Resolving the issue of unions of overloaded methods remains complex. There may be merit in auto-widening to a readonly-array-of-unions specifically for arrays. This manual workaround is generally safe and practical:

const arr2: readonly (string | number)[] = arr; // okay
arr2.every(() => true); // okay

Yet, it's not implemented presently.

Playground link for code exploration

Answer №2

The reasoning behind this relates to the explanation of every()

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

When dealing with a union type like yours, the signature transforms into

{
  <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[];
}

The issue arises from the incompatible types within the predicates.

Answer №3

After exploring the discussion in the comments, it is evident that every includes an overload with a generic function containing a type predicate while some lacks such functionality.

What causes the current "incompatibility" of function signatures?

  • There seems to be a connection to their generic nature. However, a concrete answer still eludes me. Perhaps @jcalz can provide a satisfying explanation :)

Why does every possess generics and a type predicate?

  • In contrast to some, invoking every can impact the array's type. For instance, when you have an array like

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

    Running every on it to verify if every element is a number could potentially alter the resulting array's type.

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

    This alteration is facilitated by using type predicates. Therefore, for type predicates to exert influence here, every must be generic. (Otherwise, the type predicate would be disregarded by every)


So how can this issue be addressed?

I propose modifying the type of input to an intersection of (number[] | string[]) and (number | string)[]. Although unconventional, this adjustment resolves the error at hand.

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

The function retains its original calling behavior.

// 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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Conditional types can be used as type guards

I have this simplified code snippet: export type CustomType<T> = T extends Array<unknown> ? {data: T} : T; function checkAndCast<T extends Array<unknown>>(value: CustomType<T>): value is {data: T} { return "data" ...

What is the best way to pass individual values from an array as arguments to a function?

Here is a function that can take in multiple integer parameters and return their sum: int calculateSum(int num, ...){ va_list numbers; va_start(numbers, num); int result = 0; for(int i=0; i<num; i++) { result += va_arg(numbers, i ...

Expanding declaration files in TypeScript to include third-party libraries

Is there a way to customize third-party declaration files? For instance, I am looking to enhance Context from @types/koa by adding a new field (resource) to it. I attempted the following: // global.d.ts declare namespace koa { interface Context { ...

Tips for mocking a module with a slash character in its name?

When it comes to mocking a standard npm project, the process is simple. Just create a __mocks__ folder next to the node_modules folder, then name the file after the package and insert the mock contents. For example: /__mocks__/axios.ts However, I encount ...

Modify color - Angular Pipe

I needed to make 2 transformations on my Angular template. The first transformation involved changing the direction of the arrow depending on whether the number was negative or positive. I achieved this using a custom Pipe. The second transformation was t ...

Exploring the depths of array dimensions in the C++ programming

Even though arrays need to be defined with a fixed size, in the code provided, the variable i exceeds the size of the array due to the addition of 2 in "i<sz+2" in the for loop. Surprisingly, the code does not produce any errors. Why is that? Another qu ...

Having trouble passing a React Router Link component into the MuiLink within the theme

The MUI documentation explains that in order to utilize MuiLink as a component while also utilizing the routing capabilities of React Router, you need to include it as a Global theme link within your theme. An example is provided: import * as React from & ...

Loop through each item in the collection with attributes that contain the character '@'

Below is a data dump of $v containing an object: object(SimpleXMLElement)[69] public '@attributes' => array 'identifier' => string 'FC7C5117-8FF9-4FF4-86D2-F139EDE6EA74-19726-00011178F6D7A5AC' (length=59) ...

Generate a JSON object by extracting variables from an array

I'm struggling to create a JSON object with an array due to an issue where only the last index value is assigned to my variable. Can someone guide me on how to append all loop iterations to one variable? Despite successfully looping through my array ...

Mapping imports in TypeScript refers to the process of linking external dependencies or modules

Imagine in the world of typescript, whenever I write an import statement like this import styles from "./styles.module.scss"; I want typescript to actually import this file ./styles.module.scss.json The reason for this is that a JSON object con ...

Show information in the Activity view upon clicking

My application retrieves a Json Object / Array from an API and displays it in the view. For each array item, I display the id and name in my MainActivity. Each array data item is associated with a button. When the button is clicked, the application should ...

Unable to send JSON data from server to client following a successful file upload operation

I'm currently working on a project that involves NodeJS, Express, JQuery, and Typescript. The issue I'm facing is related to uploading a file from the front end, which is successful. However, I'm encountering difficulties in returning a JSON ...

Having trouble retrieving elements from combined arrays

After being advised to merge 2 objects together, I used the array_merge function. However, I am now facing an issue where I can't access the second object from the merged result. The keys that appear after the merge process look strange. Have I made a ...

Why does the ReactJS MaterialUI Modal fail to update properly?

Recently, I encountered a curious issue with my Modal component: https://i.stack.imgur.com/dkj4Q.png When I open the dropdown and select a field, it updates the state of the Object but fails to render it in the UI. Strangely, if I perform the same action ...

Breaking up an array of objects into separate arrays based on a specific key using JavaScript

Context: Seeking assistance in developing a timetable planner that can detect time clashes. Any guidance or support is greatly appreciated. Specific Issue: Struggling to determine how to divide my array of objects into multiple arrays with a specific key ...

One can only iterate through the type 'HTMLCollection' by utilizing the '--downlevelIteration' flag or setting a '--target' of 'es2015' or above

I'm currently working on developing a loader for my static grid. I've incorporated the react-shimmer-skeleton package source code, but I'm encountering issues with eslint in strict mode. You can find the respective repository file by followi ...

The correct method to effectively type out a JSON object

Recently, I came across an article on that mentioned the possibility of importing JSON objects into a TypeScript project. Intrigued, I decided to give it a try by importing the following JSON: { "defaultLanguage": "en", "languageMap": { "en": "En ...

What is the most efficient way to perform an inline property check and return a boolean value

Can someone help me with optimizing my TypeScript code for a function I have? function test(obj?: { someProperty: string}) { return obj && obj.someProperty; } Although WebStorm indicates that the return value should be a boolean, the TypeScript compil ...

Is there a method in AngularJS to compel TypeScript to generate functions instead of variables with IIFE during the compilation process with gulp-uglify?

My AngularJS controller looks like this: ArticleController.prototype = Object.create(BaseController.prototype); /* @ngInject */ function ArticleController (CommunicationService){ //Some code unrelated to the issue } I minified it using gulp: retur ...

Utilizing TypeScript to Retrieve the Parameter Types of a Method within a Composition Class

Greetings to the TS community! Let's delve into a fascinating problem. Envision having a composition interface structured like this: type IWorker = { serviceTask: IServiceTask, serviceSomethingElse: IServiceColorPicker } type IServiceTask = { ...