Error in TypeScript when utilizing generic callbacks for varying event types

I'm currently working on developing a generic event handler that allows me to specify the event key, such as "pointermove", and have typescript automatically infer the event type, in this case PointerEvent. However, I am encountering an error when trying to use more than one event.

For a brief example of the issue, you can check out this example

export type ContainedEvent< K extends keyof HTMLElementEventMap> = {
    eventName: K;
    callback: ContainedEventCallback< K>;
  
};
export type ContainedEventCallback< K extends keyof HTMLElementEventMap> = (
    event: HTMLElementEventMap[K],

) => void;
export default function useContainedMultiplePhaseEvent<
    K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap
>(
    el: HTMLElement ,
    events: ContainedEvent<K>[],
) {
    
  for (const e of events) {
      el.addEventListener(e.eventName, (ev) => e.callback(ev));
  }     
}
const div = document.createElement("div");
 const doA: ContainedEventCallback<"pointerdown"> = (
        e,
    ) => {
      console.log("A")
    };
 const doB: ContainedEventCallback<"pointermove"> = (
        e,
    ) => {
      console.log("B")
    };

useContainedMultiplePhaseEvent(div,
        [
            {
                eventName: "pointerdown",
                callback: doA,
            },
            {
                eventName: "pointermove",
                callback: doB,
            }
        ]
    );

Answer №1

It appears that the main issue at hand lies in how TypeScript deduces a generic element type from an array literal. The inference process only takes into account the first element of the array, which is usually desirable as it enforces homogeneity. This means that a function like foo<T>(...args: T[]) {} can accept inputs like

foo("a", "b", "c")
and foo(1, 2, 3), but not
foo("a", 2, "c")
. In your scenario, however, you require a heterogeneous array.

To address this, the approach typically involves modifying the generic type parameter to encompass the entire array rather than just the element type. In your case, adapting K to reference the tuple of type arguments for a tuple of ContainedEvents could resolve the issue. This would mean defining K as something like

["pointerdown", "pointermove"]
and specifying that events should be of type
[ContainedEvent<"pointerdown">, ContainedEvent<"pointermove">]
.

Consider this implementation:

function useContainedMultiplePhaseEvent<K extends readonly (keyof HTMLElementEventMap)[]>(
    el: HTMLElement, events: [...{ [I in keyof K]: ContainedEvent<K[I]> }],
) {
    for (const e of events) {
        el.addEventListener(e.eventName, (ev) => e.callback(ev));
    }
}

In this function, the type of events becomes a mapped tuple type where each element indexed by I references K[I] wrapped with ContainedEvent, denoted as ContainedEvent<K[I]>. Additionally, the type of events is enclosed in a variadic tuple type [...⋯] to instruct the compiler to infer its type as a tuple instead of an unordered array.

Testing this solution:

useContainedMultiplePhaseEvent(div, [
    { eventName: "pointerdown", callback: doA, },
    { eventName: "pointermove", callback: doB, }
]); // successful
// useContainedMultiplePhaseEvent<["pointerdown", "pointermove"]>

The results are promising!


This response addresses the original query point raised.

While alternative approaches exist, I opted to stay close to your initial code structure. Since K in ContainedEvent<K> primarily pertains to a fixed union keyof HTMLElementEventMap, transforming ContainedEvent itself into a union might be feasible. This could involve altering the definition to a distributive object type following guidelines from ms/TS#47109. Consequently, the useContainedMultiplePhaseEvent function may no longer need to be generic, as each element within events would simply adhere to the union type ContainedEvent.

type ContainedEvent<K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap> =
    { [P in K]: {
        eventName: P; callback: ContainedEventCallback<P>;
    } }[K];

function useContainedMultiplePhaseEvent(el: HTMLElement, events: ContainedEvent[]) {
    events.forEach(<K extends keyof HTMLElementEventMap>(e: ContainedEvent<K>) =>
        el.addEventListener(e.eventName, (ev) => e.callback(ev)));
}

This revised version also delivers the intended functionality. For further insights into how a distributive object type operates, refer to ms/TS#47109 or relevant resources available online.

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

One issue with AngularJs is that it does not accurately display data that has been modified within

My MediaService service is being modified within a component. The data in MediaService is connected to another component, but any changes made in the first component are not reflected in the HTML of the second component. MediaService angular .module(&apo ...

Ways to efficiently update the API_BASE_URL in a TypeScript Angular client generated by NSwag

Is it possible to dynamically change the API_BASE_URL set in my TypeScript client generated by NSWAG? I want to be able to utilize the same client with different API_BASE_URLs in separate Angular modules. Is this achievable? Thank you for your assistance. ...

Bootstrap modal experiencing technical difficulties

I am experiencing issues with my bootstrap modal. It seems to be malfunctioning, almost as if the CSS or JavaScript files are not being recognized. I have tried various solutions but have been unable to resolve the problem. I even attempted using the examp ...

Is it possible to transfer an image from the client to the NodeJS server and store it locally within the server itself?

Is there a way for me to upload an image from the client side, send it via an HTTP request (POST) to the server (NodeJS), and save it internally on the server? Whether using Jquery, XMLHttpRequest, or a form, I continue to face the same issue where I can& ...

Typescript's Nested Type Assignments

Simply put, I'm making an API call and receiving the following data: { getUserInfo: { country: 'DE', email: '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3c48594f487c59445d514c5059125f5351">[e ...

updating the page using the latest AJAX function

I have a webpage that showcases the newest images. I am utilizing an AJAX request to organize these images : $('#trier').on('click', function() { // Clicking on the button triggers sorting action var b = document.getElementByI ...

Leveraging electron-usb alongside electron

I attempted to integrate the electron-usb library into my electron project. Upon running npm start with require('electron-usb') in my index.html file, an error is displayed in the console: Uncaught Error: The specified procedure could not be fo ...

Solving runtime JavaScript attribute issues by deciphering TypeScript compiler notifications

Here is a code snippet I am currently working with: <div class="authentication-validation-message-container"> <ng-container *ngIf="email.invalid && (email.dirty || email.touched)"> <div class="validation-error-message" *ngIf=" ...

The $.Get method does not retain its value within an each() loop

When using the jQuery each() method on a DropDown select, I iterate through an li element. However, my $.get() function takes time to fetch data from the server, so I use a loading image that toggles visibility. The issue is that the each() method does not ...

Ensure that this regular expression is able to accurately match all numbers, even those without decimal points

Seeking help to create a script that can extract the negative percentage value between two numbers. One of the numbers is dynamic and includes different currencies, decimals, etc., so I believe a regex is needed for this task. The current script works, but ...

What is the best way to transfer information between two views in Aurelia?

I have two different perspectives, one called logo and the other called folder. The logo view should display something from the folder view if the folder is empty. logo.html <template> <require from="company-assets/folders"></require> ...

Enabling Bootstrap modal windows to seamlessly populate with AJAX content

I'm currently in the process of crafting a bootstrap modal that displays the outcome of an AJAX request. Below is my bootstrap code: {{--Bootstrap modal--}} <div id="exampleModal" class="modal" tabindex="-1" role="dialog"> <div class="m ...

What are the differences between a Chrome app and extension? Is there any other way to access the tabs currently open in your

I need to develop an app that can access the tabs a user has open, but I'm struggling to find a way to do so without having my app run in Chrome itself. Creating an extension restricts the UI significantly, which is problematic since my app requires a ...

Using External APIs in React: A Guide

Recently, I created a React app using the npm module 'create-react-app' I ran into an issue where I needed to call an external API from api.example.com, but found that Axios was making requests to localhost instead of the external API. Here is ...

Currently in the process of creating a carousel displaying images, I have encountered an issue stating: "An optional property access cannot be on the left-hand side of an assignment expression."

I am currently working on an Angular Image Carousel that utilizes a model to iterate through the images. However, I am encountering an error when attempting to access the first position. An error message stating "The left-hand side of an assignment expres ...

How does the PhoneGap API handle Timestamp format?

Within the realm of app development, phoneGap offers two vital APIs: Geo-location and Accelerometer. Both these APIs provide a timestamp in their onSuccess method. In Accelerometer, the timestamp appears as '1386115200', whereas in Geo-location i ...

Efficiently transferring input to a Typescript file

Is there a better way to capture user input in Angular and pass it to TypeScript? <form > <input #input type="text" [(ngModel)]="inputColor" (input)="sendInput(input.value)" /> </form> The current method involves creating a ...

The specified class is not found in the type 'ILineOptions' for fabricjs

Attempting to incorporate the solution provided in this answer for typescript, , regarding creating a Line. The code snippet from the answer includes the following options: var line = new fabric.Line(points, { strokeWidth: 2, fill: '#999999', ...

Navigate through the rows and columns of an HTML table by utilizing Javascript with webdriver, specifically for Protractor in a non-angular environment

I need help with iterating through rows and columns using Selenium Webdriver. I am currently using Protractor for a non-angular web page. I have some Java code that works with the WebElement class to get the number of links, but now I need to transition th ...

Supplying information to my ejs template while redirecting

I am currently working on a feature that involves sending data from the login page to the home page when the user is redirected. This data will then be used in the home EJS file. Below is the code snippet I have implemented: module.exports = functio ...