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

Is it possible for an uninitialized field of a non-null literal string type to remain undefined even with strict null checks in

It seems that there might be a bug in Typescript regarding the behavior described below. I have submitted an issue on GitHub to address this problem, and you can find it at this link. The code example provided in that issue explains the situation more clea ...

Converting a string into a TypeScript class identifier

Currently, I am dynamically generating typescript code and facing an issue with quotes in my output: let data = { path: 'home', component: '${homeComponentName}', children:[] }; let homeComponentName = 'HomeComponent' ...

Tips for connecting data to an HTML page with Angular 2

My code is throwing an error message while I'm debugging: Error: Uncaught (in promise): TypeError: Unable to get property 'standard' of undefined or null reference Here is the relevant part of my code: student.component.ts: this._studentSe ...

Unable to locate the reference to 'Handlebars' in the code

I am currently attempting to implement handlebars in Typescript, but I encountered an error. /// <reference path="../../../jquery.d.ts" /> /// <reference path="../../../require.d.ts" /> My issue lies in referencing the handlebars definition f ...

How do React Native proxies differ from vanilla React proxies in their functionality?

Trying to retrieve data from an API running locally on port 5000 of my device, I recalled setting the proxy in package.json when working on React web applications: "proxy": "https://localhost:5000" Attempting to fetch information f ...

Set up your Typescript project to transpile code from ES6 to ES5 by utilizing Bable

Embarking on a new project, I am eager to implement the Async and Await capabilities recently introduced for TypeScript. Unfortunately, these features are currently only compatible with ES6. Is there a way to configure Visual Studio (2015 Update 1) to co ...

Transitioning to TypeScript: Why won't my function get identified?

I am in the process of transitioning a functional JavaScript project to TypeScript. The project incorporates nightwatch.js Below is my primary test class: declare function require(path: string): any; import * as dotenv from "dotenv"; import signinPage = ...

Mapping a response object to a Type interface with multiple Type Interfaces in Angular 7: A step-by-step guide

Here is the interface structure I am working with: export interface ObjLookup { owner?: IObjOwner; contacts?: IOwnerContacts[]; location?: IOwnerLocation; } This includes the following interfaces as well: export interface IObjOwner { las ...

Updating the state of Formik

Currently, I'm knee-deep in a React project that requires a slew of calculations. To manage my forms, I've turned to Formik, and for extra utility functions, I've enlisted the help of lodash. Here's a peek at a snippet of my code: impor ...

Troubleshooting Puppeteer compatibility issues when using TypeScript and esModuleInterop

When attempting to use puppeteer with TypeScript and setting esModuleInterop=true in tsconfig.json, an error occurs stating puppeteer.launch is not a function If I try to import puppeteer using import * as puppeteer from "puppeteer" My questi ...

What causes the website to malfunction when I refresh the page?

I utilized a Fuse template to construct my angular project. However, upon reloading the page, I encountered broken website elements. The error message displayed is as follows: Server Error 404 - File or directory not found. The resource you are looking fo ...

Step-by-step guide to setting up a TypeScript project on Ubuntu 15 using the

As a newcomer to UBUNTU, I have recently ventured into learning AngularJS2. However, when attempting to install typescript using the command: NPM install -g typescript I encountered the following error message: view image description here ...

Is there a way for me to loop through an object without prior knowledge of its keys?

Upon receiving data from the server, it looks something like this: { "2021-10-13": { "1. open": "141.2350", "2. high": "141.4000", "3. low": "139.2000", "4. close& ...

Utilizing Logical Operators in Typescript Typing

What is the preferred method for boolean comparisons in Typescript types? I have devised the following types for this purpose, but I am curious if there is a more standard or efficient approach: type And<T1 extends boolean, T2 extends boolean> = T1 ...

What is the best way to invoke a function in a class from a different class in Angular 6?

Below is the code snippet: import { Component, OnInit, ViewChild } from '@angular/core'; import { AuthService } from '../core/auth.service'; import { MatRadioButton, MatPaginator, MatSort, MatTableDataSource } from '@angular/mater ...

Looking for a regular expression to verify if the URL inputted is valid in TypeScript

After conducting thorough research, I discovered that none of the suggested URLs met my criteria, prompting me to raise a new query. Here are my specific requirements: * The URL may or may not include 'http' or 'https' * The URL can co ...

In Javascript, determine the root cause of a boolean expression failing by logging the culprit

Within my JavaScript code, I have a complex predicate that comprises of multiple tests chained together against a value. I am looking for a way to log the specific location in the expression where it fails, if it does fail. Similar to how testing librarie ...

Issue with Redis cache time-to-live not adhering to set expiration

I have encountered an issue while using IoRedis and DragonflyDB to implement rate limiting in my web application. Despite setting a TTL of 5 seconds for the keys in the Redis DB, sometimes they do not expire as expected. I am struggling to understand why t ...

The incorrect argument is being used to infer the generic type

I have implemented the NoInfer feature from the library called ts-toolbelt. However, it seems that the example provided below does not reflect its intended effect. In this scenario, the generic type is deduced from the util function as soon as it is intr ...

Can someone explain the meaning of this syntax in a TypeScript interface?

Just the other day, I was trying to incorporate the observer pattern into my Angular 4 application and came across this TypeScript code snippet. Unfortunately, I'm not quite sure what it means. Here's the code: module Patterns.Interfaces { ...