Managing a scenario with a union type where the value can be retrieved from one of two different functions

There are two similar methods that I want to refactor to eliminate redundant code. The first function returns a single element, while the second function returns multiple elements:

//returns a single element
const getByDataTest = (
  container: HTMLElement,
  dataTest: string
) => {
  const element = queryByAttribute(
    'data-test',
    container,
    dataTest
  );

  if (!element) {
    throw queryHelpers.getElementError(
      `Unable to find element by: [data-test="${dataTest}"]`,
      container
    );
  }
  return element;
};

//returns multiple elements
const getAllByDataTest = (
  container: HTMLElement,
  dataTest: string
) => {
  const elements = queryAllByAttribute(
    'data-test',
    container,
    dataTest
  );

  if (elements.length === 0) {
    throw queryHelpers.getElementError(
      `Unable to find any elements by: [data-test="${dataTest}"]`,
      container
    );
  }
  return elements;
};

To simplify this code, I aim to combine these functions into one by introducing a multiple argument that toggles between using either of the query methods:

const getDataTest = (
  container: HTMLElement,
  dataTest: string,
  multiple = false
) => {
  //choose which query method to use
  const queryMethod = multiple ? queryHelpers.queryAllByAttribute : queryHelpers.queryByAttribute;
  const result = queryMethod(
    'data-test',
    container,
    dataTest
  );

  if ((multiple && result.length === 0) || !result) {
    throw queryHelpers.getElementError(
      `Unable to find any element by: [data-test="${dataTest}"]`,
      container
    );
  }
  return result;
};

The issue arises when handling the result variable of type HTMLElement | HTMLElement[], causing concerns with accessing result.length. Various attempts have been made to address this error without success.

Answer №1

When working with a union type in TypeScript, indexing into a value with a key that doesn't exist on all members of the union is not allowed:

let result: HTMLElement | HTMLElement[];
result = Math.random() < 0.5 ? document.body : [document.body];

result.length // will cause an issue

The reason for this restriction is that object types are open and extendible, so an HTMLElement could potentially have a length property of an unknown type:

const possibleResult = Object.assign(document.createElement("div"), { length: "whaaa" });
result = possibleResult;

console.log(possibleResult.length) // "whaaa", not a number

This means you can't assume the presence or type of a length property in a union type without checking first.


In TypeScript, the correct approach is to use a type guard to narrow the type of result to either HTMLElement or HTMLElement[]. One way to achieve this is by using Array.isArray():

if (Array.isArray(result)) {
  result.map(x => x.tagName) // works fine
} else {
  result.tagName // also works fine
}

Another option is to use the in operator as a type guard:

if ("length" in result) {
  result.map(x => x.tagName) // still works
} else {
  result.tagName // also works
}

While convenient, the in operator may lead to unsound behavior, as it allows narrowing based on properties that do not guarantee the type of the variable. However, the TypeScript team intentionally designed it this way to provide flexibility for developers.


There are various methods available to narrow unions in TypeScript, so choose the one that best fits your scenario.

Playground link to code

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

Learn how to utilize a Library such as 'ngx-doc-viewer2' to preview *.docx and *.xlsx files within the application

After 3 days of searching, I finally found a solution to display my *.docx and *.xlxs files in my angular application. The API returns the files as blobs, so my task was to use that blob to show the file rather than just downloading it using window.open(bl ...

What is the best way to have an icon appear when a child component div is clicked, without it displaying on other similar divs?

Within my child component div, I have configured it to display information from an object located in the parent component. Initially, when the web app loads, it correctly shows three divs with names and messages retrieved from the created object. However, ...

What is the best way to set the typing of a parent class to the child constructor?

I am seeking a method to inherit the parameter types of a parent's constructor into the child's constructor. For example: class B extends A { constructor (input) { super(input); } } I attempted the following: class B extends ...

Incorporate service providers into models with Ionic3/Angular4

I am seeking feedback from individuals with more experience than me to determine if my approach is correct. I am currently working on an Ionic3-Angular app that involves a CRUD functionality for "Clientes". From what I have researched, the recommended st ...

Creating a TypeScript function that can dynamically assign values to a range of cells within a column, such as AD1, AD2, AD3, and so on

Hello there I'm currently working on a function that will dynamically assign values to the column range of AE to "AD" + i. However, when I use the function provided below, it only writes AD5 into the first 5 columns instead of AD1, AD2, AD3, and so o ...

Steps for modifying the value of a field within an Angular formGroup

Is there a way to update the value of the "send_diagnostic_data" field in a separate function? private generateForm(): void { this.messageForm = new FormGroup({ message: new FormControl(''), date: new FormControl(new Date()), messag ...

Is there a way to utilize an AXIOS GET response from one component in a different component?

I'm having trouble getting my answer from App.tsx, as I keep getting an error saying data.map is not a function. Can anyone offer some assistance? App.tsx import React, {useState} from 'react'; import axios from "axios"; import {g ...

PhpStorm IDE does not recognize Cypress custom commands, although they function properly in the test runner

Utilizing JavaScript files within our Cypress testing is a common practice. Within the commands.js file, I have developed a custom command: Cypress.Commands.add('selectDropdown', (dropdown) => { cy.get('#' + dropdown).click(); } ...

What is the correct way to trigger an event specified as a string parameter in the emit() function?

My current goal is to pass the emit name as a string (for example, 'showComponent') from child to parent. I then want to trigger another emit in the emitAction(callbackName: string) function, and finally execute the showComponent() function. I&a ...

Setting the type of a prop dynamically based on another prop value

Consider the following scenario with an interface: interface Example { Component: React.ReactElement; componentProperties: typeof Example.Component; } Is there a way to determine the type of properties expected by a passed-in custom component? For ...

Exploring the power of nested components within Angular 2

I am encountering an issue with a module that contains several components, where Angular is unable to locate the component when using the directive syntax in the template. The error message I receive states: 'test-cell-map' is not a known elemen ...

The error message "Type 'IPromise<{}>' is not compatible with type 'IPromise<TemplatesPagingModel>' in typescript version 2.8.0" is displayed

Currently, I am working on an AngularJS framework (version 1.5.8) with the latest TypeScript files (version 2.8.0). However, after updating to the most recent TypeScript version, the code below is not compiling. Implementation of Angular interface: inter ...

Is it possible to pass a Styled Components Theme as Props to a Material UI element?

After spending 9 hours scouring the internet for a solution, I am at my wit's end as nothing seems to work. Currently, I am developing a React component using TypeScript. The issue lies with a simple use of the Material UI Accordion: const Accordion ...

Differentiating between model types and parameters in Prisma can greatly enhance your understanding of

Consider the following scenario: const modifyData = async(data, settings) => { await data.update(settings) } In this case, the data refers to any data source, and the settings consist of objects like where and options for updating the data. How can ...

Implement a personalized Laravel Dusk selector with the attribute data-dusk

In the world of Laravel Dusk, the default selector hunts for the dusk="something" attribute in your HTML. If you want to dive deeper into this topic, check out this resource. However, when it comes to compatibility with Typescript for React/Vue, ...

Choosing a single item from multiple elements in React using React and typescript

In this particular project, React, TypeScript, and ant design have been utilized. Within a specific section of the project, only one box out of three options should be selected. Despite implementing useState and toggle functionalities, all boxes end up bei ...

Sending binary information from a .net core web api to a typescript application

I currently have a .net core 3.0 web api application connected to an angular 8 client. While I have successfully transferred data between them using json serialization, I am now looking for a way to transfer a bytes array from the api to the client. After ...

Error in React-Typescript: The element type 'Component' is missing any construction or call signatures

I recently wrote a higher order component using React withContext: import React from 'react'; import permissionContext from 'context'; interface Props { Component: () => React.Component; } const withContext: React.FC<Props> ...

It appears that TypeScript is generating incorrect 'this' code without giving any warning

I seem to be facing some resistance filing a feature request related to this on GitHub issues, so I'll give it a shot here. Here is the code snippet that caused me trouble: export class Example { readonly myOtherElement: HTMLElement; public ...

Monitor a universal function category

Trying to implement a TypeScript function that takes a single-argument function and returns a modified version of it with the argument wrapped in an object. However, struggling to keep track of the original function's generics: // ts v4.5.5 t ...