Narrowing types for arrays based on their discriminants

Is there a way to narrow the type string[] | number[] to one of these array types without explicitly using a function that returns something like T is number[]?

I attempted this, however, TypeScript (version 5.5.4) did not comprehend it: playground

declare const arr : string[] | number[]

if (arr.length) {
  if (typeof arr[0] === 'string') {
    // Property 'substr' does not exist on type 'string | number'.
    //   Property 'substr' does not exist on type 'number'
    arr.map(x => x.substr(1))
  } else {
    // Operator '+' cannot be applied to types 'string | number' and 'number'.
    arr.map(x => x + 1)
  }
}

Answer №1

In TypeScript, narrowing an object type by narrowing one of its properties is not a standard behavior. If you want this functionality, there is an open feature request for it on the GitHub page at microsoft/TypeScript#42384. Currently, the only way to narrow the type of an object based on a property check is if the object's type is a discriminated union and you check a discriminant property. However, a union like string[] | number[] is not a discriminated union because the discriminant property must be a literal type. This means that neither string nor number are literal types, so checking typeof arr[0] === "string" won't narrow down arr.


To achieve narrowing in such cases, you can create a type guard function. One approach is to simulate the desired behavior of narrowing an object through a property check with the following function:

function unionPropGuard<T, K extends keyof T, U extends T[K]>(
  obj: T, key: K, guard: (x: T[K]) => x is U):
  obj is T extends unknown ? (T[K] & U) extends never ? never : T : never {
  return guard(obj[key])
}

This function filters elements of the union T based on whether the K property overlaps with the guarded type U. You can then refactor your code from using if (guard(obj[prop])) to utilizing

if (unionPropGuard(obj, prop, guard))
. In practical terms, this would look something like:

if (arr.length) {
  if (unionPropGuard(arr, 0, x => typeof x === "string")) {
    arr.map(x => x.substring(1))
  } else {
    arr.map(x => x + 1)
  }
}

It's important to note that x => typeof x === "string" is automatically inferred as a type guard function of type

(x: string | number) => x is string
. Therefore, calling unionPropGuard() accomplishes the narrowing effect you initially attempted with typeof arr[0] === "string". If the function returns true, arr is narrowed to string[]; otherwise, it narrows to number[].

Check out the Playground link for interactive code samples

Answer №2

Have you considered explicitly informing TypeScript of the type after performing the check?

You can use a type assertion like this:

declare const items : string[] | number[];

if (items.length) {
  if (typeof items[0] === 'string') {
    const stringItems = items as string[];
    stringItems.map(item => item.substr(1));
  } else {
    const numberItems = items as number[];
    numberItems.map(item => item + 1);
  }
}

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

Struggling to utilize a personalized filter leads to the error message: "Unable to call a function without a designated call signature."

Trying to use a custom filter from an Angular controller is resulting in the error message: 'Cannot invoke an expression whose type lacks a call signature'. I successfully implemented this on my last project, so I'm confused about what coul ...

What is the process for sending an http get request that provides a JSON array to populate an ngFor directive?

I'm trying to figure out how to make an http get request in order to retrieve a json array of data that I can use within an ngFor loop. The list that needs to be looped through is stored in this.list. Currently, the json file for testing purposes is l ...

Compile Node.js applications for multiple projects using Grunt

I am looking for an efficient way to build a multi-project application. Currently, my project structure looks like this: Each app is a nodejs application - parent folder (git root) |- app1 |-- app1-backend |-- app1-frontend |- app2 |- app3 Right now, I ...

Struggling to transfer information between different components?

I recently implemented a delete button functionality in my project to remove elements when clicked, but I'm facing an issue where the input decorator is not properly receiving data for deletion. When I console log, it shows that the array is empty whi ...

typescript function discrimination

const enum Tag { Friday = 'Friday', Planning = 'Planing', } const test = (tag: Tag, task:/* ??? */): string => {/* some logic */} If tag is set to Tag.Friday, then the function task should expect a parameter of type (tour: strin ...

Ways to convert a string into a Date object without using moment.js

I am facing a challenge with 2 dates that are stored in the following format: "04.12.2019, 09:35" // Today "05.12.2019, 12:50" // Another date I need to compare these dates to determine if they have passed or are yet to come. My initial approach was to ...

Determine the type of the final function within a variable number of nested closures

Imagine you have a function like this: const f = a => b => ... x => { return somevalue } Is there a way to determine the type of just the final function typeof x => { return somevalue } even if we don't know how many closures come before ...

Variable-Valued Associative Arrays

After searching tirelessly, it seems like I'm hitting a dead end in finding what I need. I can't figure out if the issue lies with certain restrictions or my inability to articulate the problem correctly. I am currently working on an Excel expor ...

Integrating jquery into an angular project

I am facing an issue setting up jquery in an angular 6 project. When I try to import it in the ts file, I encounter the following error: Error: This module can only be referenced with ECMAScript imports/exports by turning on the 'allowSyntheticDe ...

Adding a baseURI to the image src in Angular 5 is causing issues with dynamically loading images

I am currently working on Angular 5.2.1 and I am facing an issue with loading an image from a server using its source URL. Here is the HTML code: <img #image [src]="cover" class="img-fluid" alt="NO image"> And here is the TypeScript code in image- ...

What is the best way to ensure that my variable updates whenever its reference changes?

Having trouble updating an Angular2 component's variable that is tied to a variable in another service? You're not alone. I've been struggling to get it to update without constantly resetting the variable. In my component, I have the follow ...

I am facing a segmentation error, but the cause remains unknown to me

#include <stdio.h> #include <stdlib.h> #include <math.h> #define MAX_SIZE 100 I have been experimenting with a quicksort algorithm to sort an array of 2D points based on their distance from the origin. However, I encountered a segmentati ...

Unexpected behavior observed in Typescript method used as a custom filter for ng-repeat

module filterModule{ 'use strict'; export class UsersFilter { public userList: any[]; public userQuery: string; constructor(){ this.userList = [{id:1,name:'Jim',surname:'Anderson' ...

What are the steps to make a multi dimensional array in Javascript?

I am trying to generate a multidimensional array using the code below. var i = 0; $('.button_image').each(function () { buttons[i]['left'] = $(this).position().left; buttons[i]['top'] = $(this).position().top; i+ ...

Variations in assets configuration for ng serve and ng build in Angular 5

Is it possible to specify different asset arrays when using ng build? "assets": [ "assets", "favicon.ico", { "glob": "**/*", "input": "../externalDir", "output": "./app/", "allowOutsideOutDir": true } ] In my scenario, I only want ...

Issue in Three.js: Unable to render mesh generated with an array of textures

In the process of developing an innovative game centered around cubes in a virtual sandbox environment (a groundbreaking concept poised to revolutionize the gaming industry), I am currently focusing on chunk generation. This is a glimpse into my progress t ...

Securing your React app with private routes and context-based authentication

I'm currently working on implementing basic authentication for a React+Typescript application using private routes and context. In my setup, I have a login component with a button that toggles a boolean variable authenticated in the context to true up ...

Redux-toolkit payload does not recognize the specified type

While working on my project, I encountered an issue when using redux-toolkit. I have created the following reducer: setWaypointToEdit: (state, action: PayloadAction<WaypointToEditPayload>) => { let gridPolygonsData: GridPolygonsData; const { ...

When trying to access the key value of a dynamically generated object, it returns as undefined

I am facing a challenge with my student object structure... { Freshmen: [{id: 3}, {id: 5}], Sophomores: [{id: 2}, {id: 6}], Juniors: [{id: 1}, {id: 8}], Seniors: [{id: 9}, {id: 4}, {id: 7}] } My goal is to retrieve full student objects from the d ...

Angular 2's updated router feature, routerCanReuse, provides improved functionality

I'm curious about the changes in the Angular 2 router, particularly the removal of the CanReuse interface. Is there another feature in the router that can achieve the same functionality of forcing a component reload? ...