Utilizing Array.every to refine a union of array types, narrowing down the options

I need to narrow down the type of a variable that is a union of different array types in order to run specific code for each type. I attempted to use Array.every along with a custom type guard, but encountered an error in TypeScript stating "This expression is not callable," with a confusing explanation.

Here is a simplified example:

const isNumber = (val: unknown): val is number => typeof val === 'number';

const unionArr: string[] | number[] = Math.random() > 0.5 ? [1, 2, 3, 4, 5] : ['1', '2', '3', '4', '5'];

if (unionArr.every(isNumber)) { // <- Error
  unionArr;
}

TypeScript Playground

The error message reads:

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

To resolve this issue, I found that using a type assertion to convert my array to unknown[] before narrowing its type, or changing the array from string[] | number[] to (string | number)[] helped eliminate the error without compromising type safety.

However, even after these adjustments, further type assertions were needed to correctly narrow the array type:

const isNumber = (val: unknown): val is number => typeof val === 'number';

const unionArr: string[] | number[] = Math.random() > 0.5 ? [1, 2, 3, 4, 5] : ['1', '2', '3', '4', '5'];

if ((unionArr as unknown[]).every(isNumber)) { // <- No error
  unionArr; // <- Incorrectly typed as string[] | number[]
}

if ((unionArr as (string | number)[]).every(isNumber)) { // <- No error
  unionArr; // <- Type incorrectly remains as string[] | number[]
}

TypeScript Playground

A similar comparison with a non-array union did not yield any errors and successfully narrowed the type:

const isNumber = (val: unknown): val is number => typeof val === 'number';

const union: string | number = Math.random() > 0.5 ? 1 : '1';

if (isNumber(union)) {
  union; // <- Correctly typed as number
}

TypeScript Playground

While the workaround works fine, the root cause of the error still remains unclear. It seems to be related to how TypeScript interprets the typings of Array.every, and despite trying various approaches, the type assertion as unknown[] seems to be the most suitable solution. If there are better alternatives or if something could be done differently, please feel free to share your insights!

Answer №1

Let's consider a more straightforward example:

type StringOrNumberFunction = (input: string | number) => void
type NumberOrBooleanFunction = (input: number | boolean) => void
declare const justValue: StringOrNumberFunction | NumberOrBooleanFunction

// works
justValue(123)

// errors
justValue('abc')
justValue(true)

When trying to invoke a union of functions, you end up calling a function that is the intersection of those members. If unsure about the function being called, only methods supported by both functions can be used. In this scenario, since both functions accept a number, only numbers are allowed.

If the intersection of these functions would have incompatible arguments, calling the function becomes impossible. Let's illustrate this with an example:

type StringType = (value: string) => void
type NumberType = (value: number) => void
declare const functionUnion: StringType | NumberType

functionUnion(123)    // Argument of type 'number' is not assignable to parameter of type 'never'.(2345)
functionUnion('hello') // Argument of type 'string' is not assignable to parameter of type 'never'.(2345)

This closely resembles your issue.

Playground


The every method for string arrays and number arrays expects different parameters.

(value: string, index: number, array: string[]) // string[] every() args
(value: number, index: number, array: number[]) // number[] every() args

Which presents a similar challenge as discussed above.


Hence, it seems unlikely that the compiler will permit calling the every method on this union.

In such cases, I would suggest performing a type assertion for the entire array and manually iterating over it.

const isArrayOfTypeNumber = (array: unknown[]): array is number[] => {
  for (const value of array) {
    if (typeof value !== 'number') return false
  }

  return true
}

declare const unitedArray: string[] | number[]

if (isArrayOfTypeNumber(unitedArray)) {
  Math.round(unitedArray[0]); // This will work
}

Playground

Answer №2

Latest Update for July 2023

A new feature in TypeScript 5.2, which is currently being tested in beta version, introduces a change in how TypeScript handles function calls with union types, particularly when the function is related to array methods like the one mentioned in my previous query.

To learn more about this specific update, visit: Simplifying Method Usage for Union Types of Arrays

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

What is the best way to refresh a personalized form element using Angular?

I have developed a custom form control with validation that utilizes a standalone FormControl to manage the value and perform certain validations. Is there a method in Angular to reset the inner FormControl when the control is being reset from another For ...

There was a parsing error due to encountering an unexpected reserved word 'interface' in the code, as flagged

I'm encountering an issue with my code when trying to utilize Props. The error message I'm receiving is "Parsing error: Unexpected reserved word 'interface'. (3:0)eslint". This project is being developed using next with TypeScript. Er ...

Is there a way to insert a secured page right before accessing the dashboard?

I am trying to create a locked page that will display a message when users access the web app from a mobile device and load a mobile layout page displaying a message like mobile is not supported. I was considering using document.addEventListener('DOMC ...

Tips for creating a window closing event handler in Angular 2

Can someone guide me on how to create a window closing event handler in Angular 2, specifically for closing and not refreshing the page? I am unable to use window.onBeforeunLoad(); method. ...

There is an issue with the Next.js middleware: [Error: The edge runtime is not compatible with the Node.js 'crypto' module]

Struggling with a problem in next.js and typescript for the past 4 days. If anyone can provide some insight or help with a solution, it would be greatly appreciated. Thank you! -- This is my middleware.ts import jwt from "jsonwebtoken"; import { ...

Converting JSON data to PHP format

I am currently working with some JSON data: { 'total': 4744.134525437842, 'produksiHarian': [14.800870530853988, 15.639301040842536, 16.358413710544085, 16.952318230836113, 17.45055097248538, ...], 'r_squared': 0.9 ...

Scrolling the mouse wheel on Angular 2 Typescript Highcharts Highmap

I'm currently exploring if it's possible to subscribe to the zooming event in a Highmap using Angular 2 with Typescript for Highcharts/Highmap, or possibly even to a mouse wheel scrolling event. @HostListener('scroll', ['$event&a ...

The circular reference error in Typescript occurs when a class extends a value that is either undefined, not a constructor,

Let me begin by sharing some context: I am currently employed at a company where I have taken over the codebase. To better understand the issue, I decided to replicate it in a new nestjs project, which involves 4 entities (for demonstration purposes only). ...

Interface displaying auto-detected car types

I have a setup that looks like this: interface ValueAccessor<T> { property: keyof T; getPropertyValue: (value: any) => value; } I am trying to figure out how to define the correct type and replace the any when I want to provide a custom ...

Issue with Vuex getter not reflecting correct value after modifying an array

I've been delving into the world of vuex, and I'm currently working on fetching the count or an array when I make additions or removals. Below, you'll find the code snippets I'm working with. home.vue template <template> < ...

What is the best way to create a dynamic graph in amcharts during runtime?

Below is the code for Multiple Value Axes: In this code, we aim to display a graph using dynamically generated random data. When executed, nothing will happen. <script> var chart; var chartData = []; // generate some random data within a different ...

Using ngFor to Filter Tables in Angular 5

Are you in the midst of implementing multiple data filters in an Angular Application that displays data using a Table and ngFor? You may have explored different methods such as using Pipe in Type Script, but discovered that it is not recommended accordin ...

Pass the type of object property as an argument in the function

I've been having trouble trying to figure this out and haven't been able to find a solution in the TS docs or examples I came across. Essentially, I'm working with a configuration specifying operations on object properties and looking to en ...

After the array is printed, the foreach loop is failing to function as expected

I attempted to loop through the given array. Array ( [mech_info] => Array ( [make] => Amaka [0] => Array ( [year] => 2001 [model] => Array ...

Angular allows for a maximum time span of 60 days between two date inputs

I am looking to implement a validation in JavaScript or TypeScript for Angular where the start date cannot be more than 60 days after the end date is entered. The requirement is to enforce a "maximum range of 60 days" between the two input dates (dateFro ...

Preventing the compilation process from overwriting process.env variables in webpack: A step-by-step guide

Scenario I am currently working on developing AWS Lambda functions and compiling the code using webpack. After reading various articles, I have come to know that the process.env variables get automatically replaced during compilation. While this feature ...

Adding typing to Firebase Functions handlers V2: A step-by-step guide

Here's a function I am currently working with: export async function onDeleteVideo(event: FirestoreEvent<QueryDocumentSnapshot, { uid: string }>): Promise<any> { if (!event) { return } const { disposables } = event.data.data() ...

The variable 'string' has been declared, but it is never utilized or accessed

Currently delving into Typescript and facing an early error. Despite following a tutorial, I'm encountering multiple errors that I have attempted to comment out. Would greatly appreciate it if someone could shed some light on why these errors are occu ...

Issue with manipulating currency conversion data

Currently, I am embarking on a project to develop a currency conversion application resembling the one found on Google's platform. The main hurdle I am facing lies in restructuring the data obtained from fixer.io to achieve a similar conversion method ...

Calculating the factorial of a number using the reduce function

I've been working on a function to find the factorial of an integer and then reducing the array containing factorials (multiplying each element). For instance: factor(5) >>> [1, 2, 3, 4, 5] >>> 1 * 2 * 3 * 4 * 5 >>> 120 v ...