Using Typescript to define unions with multiple callback types

While in the process of converting my code to TypeScript, I encountered a dilemma. I created a generic foreach function that can handle arrays and objects, with different types of callbacks for iteration. However, I'm unsure how to specify the type when it accepts multiple callback options. The function should be able to return either boolean or void, and accept callbacks of (any), (any, int), or (string, any) types. Here is what I have so far:

function foreach(obj: Array<any> | Object, func: (
            ((any) => boolean) |
            ((any) => void) |
            ((any, int) => boolean) |
            ((any, int) => void) |
            ((string, any) => boolean) |
            ((string, any) => void)
))
{
    // if obj is an array ...
    if(Object.prototype.toString.call(obj) === '[object Array]') {
        // if using callback def1
        if(func.length == 1) {  
            for(let i = 0; i < obj.length; i++)  {
                if(typeof func === "function") {
                    if(!func(obj[i])) break;
                }
            }
        // if using callback def2
        } else if(func.length == 2) { 
            for(let i = 0; i < obj.length; i++) {
                if(!func(obj[i], i)) break; 
            }
        }
    // if obj is an object ...
    } else if(Object.prototype.toString.call(obj) == '[object Object]') {
        // if using callback def1
        if(func.length == 1) {
            for(let key in obj) {
                if(!obj.hasOwnProperty(key)) continue;
                if(!func(obj[key])) break; 
            }
        // if using callback def3
        } else if(func.length == 2) {
            for(let key in obj) {
                if(!obj.hasOwnProperty(key)) continue;
                if(!func(key, obj[key])) break; 
            }
        }
    }
};

Answer №1

Here is a sample code snippet for a foreach function in TypeScript:

function foreach<T>(obj : T[], func: (item: T) => void)
function foreach<T>(obj : T[], func: (item: T) => boolean)
function foreach<T>(obj : T[], func: (item: T, index: number) => boolean)
function foreach<T>(obj : T[], func: (item: T, index: number) => void)
function foreach<T>(obj : {[key: string]: T}, func: (item: T) => boolean)
function foreach<T>(obj : {[key: string]: T}, func: (item: T) => void)
function foreach<T>(obj : {[key: string]: T}, func: (key: string, item: T) => boolean)
function foreach<T>(obj : {[key: string]: T}, func: (key: string, item: T) => void)
function foreach<T>(obj : T[] | {[key: string]: T}, func : (item : T | string, index ?: number | T) => (boolean | void))
{
    // Implementation of the foreach function goes here
};

For example usage and tips on how to use intellisense with this function, you can refer to the following images:

Example of usage: https://i.sstatic.net/ykRR7.png https://i.sstatic.net/fUaL3.png

To make the most out of intellisense: https://i.sstatic.net/dI4Sp.png

Answer №2

To simplify your code, consider leveraging generic functions (functions parameterized by types, such as function foo<T>), optional arguments (using ?), and multiple function signature declarations. This approach will help eliminate the need for unions and any types.

It is advisable to split your function into two separate functions. TypeScript does not support polymorphism in terms of dispatching to different implementations at runtime. Therefore, treating arrays and objects as distinct entities and writing dedicated functions for each is a better practice than relying on pseudo-polymorphism. Be explicit in your code to enhance clarity and maintainability.

Your revised code structure would resemble the following:

function forEachArray<T>(
  array: Array<T>, 
  func?: (value: T, index?: number): boolean
): void {
  for (let i = 0; i < obj.length; i++)  {
    if (func && !func(array[i], i)) break;
  }
}

function forEachObject<T>(
  object: {[index: string]: T}, 
  func?: (value: T, key?: string): boolean
): void {
  for (let key of Object.keys(object)) {
    if (func && !func(object[key], key)) break;
  }
}

The updated implementation introduces the use of optional arguments denoted by the question mark (?) symbol and generics to ensure data homogeneity within arrays or objects. By enforcing type restrictions, you can maintain consistency and avoid errors in function calls. Optionally, you can relax these constraints by specifying forEachObject<any>.

In terms of design considerations, it's recommended to standardize the callback return type rather than allowing both boolean and void options. Additionally, you can make parameters like the index or key optional using ? in the function signature.

For enhanced functionality resembling Array#forEach, you may also include a third parameter for the context ("thisArgs") and pass the underlying array or object as an argument to the callback function.

If you prefer a unified foreach function capable of handling both arrays and objects, employing multiple function signatures preceding the implementation can achieve this goal efficiently.

// Code example for combined foreach function
// Type definition for compactness
type Hash<T> = {[index: string]: T};

function foreach<T>(
  array: Array<T>, 
  func?: (value: T, index?: number, array?: Array<T>): boolean,
  thisArg?: Object
): void;

function foreach<T>(
  object: Hash<T>,
  func?: (value: T, key?: string, object?: Hash<T>): boolean,
  thisArg?: Object
): void;

function foreach(thing, func?, thisArg?) {
  (thing instanceof Array ? forEachArray : forEachObject)(thing, func, thisArg);
}

Note that when defining multiple function signatures, you are not required to specify types in the final implementation, as they would be disregarded during execution.

Please bear in mind that the code snippets provided have not been tested or compiled yet.

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 could be causing the error in the console when I try to declare datetime in Ionic?

I am just starting out with Ionic and Angular, but I seem to have hit a roadblock. The compiler is throwing an error that says: node_modules_ionic_core_dist_esm_ion-app_8_entry_js.js:2 TypeError: Cannot destructure property 'month' of '(0 , ...

Implementing a hamburger menu and social media sharing buttons on websites

Currently working on my personal website and delving into the world of Web Development, I have some inquiries for seasoned developers. My first query revolves around incorporating a hamburger menu onto my site for easy navigation to other pages. After att ...

JavaScript - Attempting to retrieve data using AJAX

Struggling with extracting data from an AJAX call using jQuery while implementing RequireJS for better maintainability and modularity in my JavaScript. Still new to RequireJS so not entirely sure if I'm on the right track. Just aiming to keep my JS mo ...

Is it possible to show or hide a DIV based on the text content of another DIV using JavaScript in

I am looking for a way to dynamically hide a DIV based on user roles using only the text inside a title tag as a reference. Here is an example of the HTML structure: <title>admin</title> If the user role is admin, then hide the following DI ...

html input type called array named AmountMap[1] is unable to be configured with <input type="textbox" size="15" name="amountMap[1]" value="0" >

**Having trouble setting the value of an HTML input type named like an array AmountMap[1] <input type="textbox" size="15" name="amountMap[1]" value="0" > When trying to set amountMap[1].value='10', ...

I am curious about the inner workings of the `createApplication()` function in the ExpressJS source code. Can you shed some light on

My goal is to deeply comprehend the inner workings of the Express library, step by step. From what I gather, when we import and invoke the 'express()' function in our codebase, the execution flow navigates to the ExpressJS library and searches fo ...

Insert the user's choice as a MenuItem within the Select component

I currently have a default list of options for the user. However, I want to allow users to add their own category dynamically. This will trigger a dialog box to appear. How can I modify my code so that the value property starts from number 4? Take a look ...

My attempt at creating a straightforward sorting function turned out to be ineffective

My attempt at creating a basic sorting function seems to be failing as it is still returning the original list instead of the sorted one. function sortByPopular (collection) { let items = collection.slice(); items.sort(function(a,b) { re ...

What is the best way to import a file in meteor version 1.3?

Trying to incorporate react-datepicker into a meteor 1.3 and react project has been quite successful so far. The only issue I am facing is the inability to access any of the css styles from the package. According to the readme, I'm supposed to requir ...

What is causing my AJAX Contact Form to navigate away from the original page?

I configured a contact form on my website more than 5 years ago, and I vividly remember that it used to show error/success messages directly on the page within the #form-messages div. However, recently, instead of staying on the contact form page and displ ...

Import an array of dynamic modules with Webpack that are known during compilation

For my project, I have the requirement to import specific modules whose actual paths are only known during compile time. Imagine having components/A.js, components/B.js, and components/C.js. In my App.js, I need to include a subset of these modules that w ...

Issue with Refreshing onRowAdd in React Material Table

I am currently using Material Table to display my table data. However, when I use the onRowAdd function to add a new row, the page does not refresh properly. Instead, it reloads and gets stuck, requiring me to manually refresh again. newData => ...

The parameter type 'IScriptEditorProps' does not accept arguments of type 'string'

After trying numerous solutions, I decided to integrate this script editor into a SharePoint site. However, when attempting to implement it, I encountered an issue with the constructor lacking arguments. Despite my efforts to troubleshoot, I have been unab ...

Using a variable in Ajax URL with Action razor syntax

I have a function that calls a method in the controller through an Action URL. However, I need to use a parameter as the name of the method, but unfortunately, this is not possible in the current implementation. function changeDropDownList(id, dropNameToC ...

Tips for implementing an if else statement in ReactJS while utilizing the useEffect hook

Below is the code snippet for returning a Plotly graph. I would like to display a message or alternative layout when there is no data available for the graph, such as showing "No data available". How can I achieve this? import React, { useEffect } from ...

Establishing a default value as undefined for a numeric data type in Typescript

I have a question regarding setting initial values and resetting number types in TypeScript. Initially, I had the following code snippet: interface FormPattern { id: string; name: string; email: string; age: number; } const AddUser = () => { ...

What is the best way to capture the output of a script from an external website using Javascript when it is returning simple text?

Recently, I decided to incorporate an external script into my project. The script in question is as follows: <script type="application/javascript" src="https://api.ipify.org"> </script> This script is designed to provide the client's IP ...

An issue of "SignatureDoesNotMatch" arises while trying to send an email using Node AWS SDK for the Simple Email Service

I am facing an issue while attempting to send an email using the @aws-sdk/client-ses SDK in Node. The error I encounter is: SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access ...

Instructions for adding an onfocus event listener to an input field in order to dynamically change the formatting of associated labels

I'm looking to change the style of my input labels to a more fancy look by implementing the .fancyclass style when using the onfocus event on the input field. I am curious to know how this can be achieved through event listeners in Javascript? ...

Is there a way to trigger an Angular $scope function from a hyperlink in the current or a new tab using a right

Here is the HTML code I am working with: <a href="" ng-click='redirectToEntity("B-",obj.id")'>Click me and Process function and then redirect</a> While this code successfully processes the function and redirects to the desired page ...