What is the best way to create a universal function that can return a promise while also passing along event

I created a specialized function that waits for an "EventEmitter" (although it's not completely accurate as I want to use it on other classes that have once but don't inherit from EventEmitter):

export function waitEvent(emitter: { once: Function }, event: string): Promise<any[]> {
    return new Promise((resolve) => {
        emitter.once(event, (...args: any[]) => {
            resolve(args);
        });
    });
}

How can I transform it into a more generic function?

For instance, imagine if Typescript is aware that the variable page can trigger an event loaded with arguments (title: string). How do I make it so that when we write

const [title] = await waitEvent(page, 'loaded')
, it understands that title will be of type string?

Here's an example from real life:

import {chromium} from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
page.once('popup', async (page) => {
    await page.click('h1');
});
const [popup] = await waitEvent(page, 'popup');
await popup.click('h1');

The code snippet using the regular .once method recognizes that page is a Page, while the one following the waitEvent considers it as any in my original version or a Worker with @jcalz's approach. This leads to the second call to .click not functioning correctly.

Answer №1

In the beginning, the basic calling signature for waitEvent() is quite simplistic:

declare function waitEvent<K extends string, A extends any[]>(emitter: {
    once(event: K, cb: (...args: A) => void): void
}, event: K): Promise<A>;

When using waitEvent() on an emitter with a compatible once() method, the compiler can infer the type of A:

const thing = {
    once(event: "loaded", cb: (title: string) => void) { }
}
const [title] = await waitEvent(thing, "loaded");
title // string

const otherThing = {
    once(event: "exploded", cb: (blastRadius: number) => void) { }
}
const [rad] = await waitEvent(otherThing, "exploded");
rad // number

However, this approach falls short in real-world scenarios where the callback parameters are interrelated and cannot be represented by simple types like the examples shown above.

We need to combine multiple call signatures into a single object that can handle different ways of invoking once().

To express this relationship in TypeScript, one common approach is using overloads with multiple call signatures:

interface MyEventEmitter {
    once(event: "loaded", cb: (title: string) => void): void;
    // ... more ... //
    once(event: "exploded", cb: (blastRadius: number) => void): void;
}

This strategy is adopted by libraries like "playwright" when directly calling once() on a value of type MyEmitter:

declare const emitter: MyEventEmitter;
emitter.once("loaded", title => title.toUpperCase()); // okay
emitter.once("exploded", rad => rad.toFixed(2)); // okay

But TypeScript struggles to determine the type of cb based on the event parameter unless explicitly called.

This limitation hampers the functionality of methods like waitEvent(), as seen in the code snippet below:

const [title] = await waitEvent(emitter, "loaded");
title // number ?!!?!

Various workarounds exist, but they come with limitations and are not entirely elegant solutions. Hardcoding the expected type of emitter in a customized version of waitEvent() might be the most practical option until TypeScript's support for higher-order functions improves.


Another alternative would involve making once() generic in a mapping interface, providing some advantages but still suffering from limitations similar to the overloaded version.

Despite its shortcomings, this representation allows inspection and possible revisions to waitEvent as demonstrated in the following example:

declare function waitEvent<
  A extends [string, (...args: any) => void], 
  K extends A[0]
>(emitter: {
    once(...args: A): void
}, event: K): Promise<Parameters<Extract<A, [K, any]>[1]>>;

const [title] = await waitEvent(emitter, "loaded");
title // string
const [rad] = await waitEvent(emitter, "exploded");
rad // number

Ultimately, understanding the nature of emitter and creating a specialized version of waitEvent() tailored to that type seems to be the most viable approach given the current constraints of TypeScript.

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

Exploring unit tests: Customizing an NGRX selector generated by entityAdapter.getSelectors()

Let's imagine a scenario where our application includes a books page. We are utilizing the following technologies: Angular, NGRX, jest. To provide some context, here are a few lines of code: The interfaces for the state of the books page: export int ...

transform json array into a consolidated array by merging identical IDs

I need to transform an array into a different format based on the values of the ID and class properties. Here is the initial array: const json = [{ "ID": 10, "Sum": 860, "class": "K", }, { "ID": 10, "Sum": 760, "class": "one", }, { "ID": ...

The React component continuously refreshes whenever the screen is resized or a different tab is opened

I've encountered a bizarre issue on my portfolio site where a diagonal circle is generated every few seconds. The problem arises when I minimize the window or switch tabs, and upon returning, multiple circles populate the screen simultaneously. This b ...

Issue: The function react.useState is either not defined or its output is not iterable

I'm facing an issue while programming in Next.js 13. Any help or suggestions would be greatly appreciated! Here are the relevant files: typingtext.tsx import React from "react"; export function useTypedText(text: string, speed: number, dela ...

Include html into typescript using webpack

Attempting to include HTML content into a variable using TypeScript and webpack has been my challenge. This is the current setup: package.json: { "devDependencies": { "awesome-typescript-loader": "^3.2.3", "html-loader": "^0.5.1", "ts-load ...

Inheritance of Generic Types in TypeScript

Could someone assist me in understanding what is incorrect with the code snippet provided here? I am still learning Typescript. interface ICalcValue { readonly IsNumber: boolean; readonly IsString: boolean; } interface ICalcValue<T> ex ...

Inheritance from WebElement in WebdriverIO: A Beginner's Guide

I am seeking a solution to extend the functionality of the WebElement object returned by webdriverio, without resorting to monkey-patching and with TypeScript type support for autocompletion. Is it possible to achieve this in any way? class CustomCheckb ...

Tips for successfully mocking axios.get in Jest and passing AxiosPromise type value

I attempted to simulate the axios.get() function using the code below, however TypeScript is returning an error stating "argument of type '{ data: expectedResult }' is not assignable to parameter of type 'AxiosPromise<{}>'". Can ...

Angular: No routes found that match the URL segment

I encountered an issue with my routes module where I am receiving the error message Cannot match any routes. URL Segment: 'edit-fighter' when attempting to navigate using the <a> link. The only route that seems to work is the champions-list ...

Converting a TypeScript array into a generic array of a specific class

I am attempting to convert a JSON source array with all string values into another array of typed objects, but I keep encountering errors. How can I correct this code properly? Thank you. Error 1: There is an issue with converting type '({ Id: string ...

What is the best way to add an extension to specific specializations of a generic type?

Is it possible to create an extension for a generic type that conforms to a protocol, but is only valid for specific specializations of the generic type? Let's take for example a protocol that calculates the frequency of values within a type conformi ...

What data structure is used to store HTML elements in TypeScript?

Currently, I am dealing with a typescript variable that holds the outcome of a query on the DOM: let games = document.getElementsByTagname("game"); My uncertainty lies in identifying the appropriate type for the resulting array. Should I expect an array ...

Displaying images dynamically in React from a source that is not public

There are 3 image options being imported, determined by the value in the state which dictates which of the 3 to display. import csv from '../../images/csv.svg'; import jpg from '../../images/jpg.svg'; import png from '../../images/ ...

A unique Angular decorator is created to seamlessly integrate a new component within an existing component

I'm currently experimenting with creating a decorator in Angular that will display a spinner on top of the main component. Essentially, my main component is making API requests and I want to overlay the decorator on top of the method. This is how I ...

The Angular 2 Router's navigation functionality seems to be malfunctioning within a service

Currently, I am facing an issue with using Angular2 Router.navigate as it is not functioning as expected. import { Injectable } from '@angular/core'; import { Http, Headers } from '@angular/http'; import { Router } from '@angular/ ...

Attempting to locate a method to update information post-editing or deletion in angular

Are there any methods similar to notifyDataSetChange() in Android Studio, or functions with similar capabilities? ...

Experiencing difficulty in triggering a NextUI Modal by clicking on a NextUI Table Row

In the process of developing my web portfolio, I am utilizing NextJS, TypeScript, and TailwindCSS. A key feature on my site involves displaying a list of books I have read along with my ratings using a NextUI table. To visualize this functionality, you can ...

When canActivate returns false, the screen in Angular 2 will still be accessed

I am encountering a problem where my canActivate method is returning false, but still navigating to the blocked screen. This issue seems to only occur in Chrome, as everything works fine in IE. Here is how the canActivate method looks: canActivate(route: ...

Error TS2339 occurs when attempting to migrate to TypeScript due to the absence of the 'PropTypes' property on the 'React' type

Currently in the process of converting a javascript/react project to a typescript/react/redux project. Encountering an issue with this particular file: import React from 'react'; import GoldenLayout from 'golden-layout'; import {Provi ...

How to apply dynamic styling to a MatHeaderCell using NgStyle?

My goal is to dynamically style a MatHeaderCell instance using the following code: [ngStyle]="styleHeaderCell(c)" Check out my demo here. After examining, I noticed that: styleHeaderCell(c) It receives the column and returns an object, however ...