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

An error persists in Reactjs when attempting to bind a function that remains undefined

I recently tested this code and everything seems to be working correctly, but the compiler is throwing an error saying 'onDismiss' is undefined. Can someone please assist me with this issue? import React, { Component } from 'react'; c ...

Preventing horizontal swiping while vertically scrolling on mobile devices

How can I prevent horizontal swipe functionality from interfering with vertical scrolling on a webpage? I have successfully blocked vertical scrolling but need help finding a solution for preventing horizontal swiping. Has anyone else encountered this issu ...

Verifying the content of the JSON data

If I receive JSON data that looks like this: {"d":1} Is it possible to determine whether the value after "d": is a 1 or a 0? I attempted the following method, but it always goes to the else block, even though I know the JSON data contains a 1. success: ...

Using JavaScript and jQuery to toggle visibility of a dynamically created input field

This script dynamically generates a group of elements consisting of four input fields. Once an element is created, you can select or deselect it, which will trigger the corresponding editor to appear. I have implemented a function to specifically hide the ...

The function signature '(event: ChangeEvent<HTMLInputElement>) => void' does not match the expected type 'ChangeEvent<HTMLInputElement>'

This is my first time using TypeScript to work on a project from the ZTM course, which was initially written in JavaScript. I am facing an issue where I am unable to set a type for the event parameter. The error message I receive states: Type '(event: ...

Information on the Manufacturer of Devices Using React Native

Struggling to locate the device manufacturer information. Using the react-native-device-info library produces the following output. There seems to be an issue with handling promises. I need to store the device manufacturer value in a variable. const g ...

Unable to abort AWS Amplify's REST (POST) request in a React application

Here is a code snippet that creates a POST request using the aws amplify api. I've stored the API.post promise in a variable called promiseToCancel, and when the cancel button is clicked, the cancelRequest() function is called. The promiseToCancel va ...

Timepicker Bootstrapping

I've been searching for a time picker widget that works well with Bootstrap styling. The jdewit widget has a great style, but unfortunately it comes with a lot of bugs. I'm on a tight deadline for my project and don't have the time to deal w ...

Strategies for injecting data into VueJS components after the page has fully loaded

My project utilizes Vue.js and Nuxt.js, and within the settings page, users can modify their personal settings. This single page allows users to navigate between tabs. <template> <div class="account-wrapper"> // Code content he ...

How can I access the marker's on-screen location in react-native-maps?

Looking to create a unique custom tooltip with a semi-transparent background that can overlay a map. The process involves drawing the MapView first, then upon pressing a marker on top of the MapView, an overlay with a background color of "#00000033" is dra ...

Transmitting information to the service array through relentless perseverance

I need assistance finding a solution to my question. Can my friends help me out? What types of requests do I receive: facebook, linkedin, reddit I want to simplify my code and avoid writing lengthy blocks. How can I create a check loop to send the same ...

html line breaks $.parseJSON

Within my website, I am currently utilizing a TinyMCE window. The method involves PHP fetching an entry from the database, decoding it as JSON, and then having in-page JavaScript parse it. However, issues arise when there are elements like style='colo ...

Do not use npm to install underscore libraries

How can I resolve the error I encountered while attempting to install packages using npm? Here is my packages file: "dependencies": { "express": "~3.3.6", "socket.io": "0.9.16", "jade": "~0.35.0", "less-middleware": "~0.1.12", "redis ...

scrollable material ui chips list with navigation arrows

I'm attempting to create a unique scrollable chips array using Material UI version 4 (not version 5). Previous examples demonstrate similar functionality: View Demo Code I would like to update the scrolling bar of this component to include l ...

Incorporating jQuery into Rails 6.1

I encountered some difficulties while setting up jQuery in rails 6.1, even though I believe it's configured correctly. Below are the steps I've taken: Installed yarn add jquery 2. In config/webpack/environments.js, I made the following changes ...

Application Initialization Error: appInits is not a valid function

When my Angular v17 application starts, I need to set some important values right away. This is how it's done in app.config.ts: export const appConfig: ApplicationConfig = { providers: [ ConfigService, ... { pr ...

Encountered an issue in React Native/Typescript where the module 'react-native' does not export the member 'Pressable'.ts(2305)

I have been struggling to get rid of this persistent error message and I'm not sure where it originates from. Pressable is functioning correctly, but for some reason, there is something in my code that doesn't recognize that. How can I identify t ...

What is the best way to conceal content within a URL while still transmitting it to the server using node.js and express?

I have been trying to figure out how to hide certain content from the URL, but still need to send that information to the server in my Express app. So far, I haven't found any solutions that work. For example, if I have a URL like www.abc.com/viewblo ...

Using an if statement to run a script in Npm

Is there a way to configure an npm run script to use different AWS accounts based on the environment? { "config": { "acc": if ({npm_config_env} == "dev") "account1" else "account_2" }, "scr ...

Interact with Datatable by clicking on the table cell or any links within the cell

When I am working with the datatable, I want to be able to determine whether a click inside the table was made on a link or a cell. <td> Here is some text - <a href="mylink.html">mylink</a> </td> Here is how I initialize my da ...