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

Original: Generic for type guard functionRewritten: Universal

I have successfully created a function that filters a list of two types into two separate lists of unique type using hardcoded types: interface TypeA { kind: 'typeA'; } interface TypeB { kind: 'typeB'; } filterMixedList(mixedList$: ...

developed a website utilizing ASP MVC in combination with Angular 2 framework

When it comes to developing the front end, I prefer using Angular 2. For the back end, I stick with Asp MVC (not ASP CORE)... In a typical Asp MVC application, these are the steps usually taken to publish the app: Begin by right-clicking on the project ...

Can you explain how to utilize multiple spread props within a React component?

I'm currently working in TypeScript and I have a situation where I need to pass two objects as props to my React component. Through my research, I found out that I can do this by writing: <MyComponent {...obj1} {...obj2} /> However, I'm fa ...

Typescript interface requiring both properties or none at all

I possess key-value pairs that must always be presented together in a set. Essentially, if I have one key with its value A:B, then there should also be another key with its value C:D. It is permissible for the object to contain neither pair as well. (An ex ...

Generics in Typescript interfaces

I'm trying to grasp the meaning of T = {} within this TypeScript interface. I've searched for documentation on this usage but haven't found anything specific. How does it differ from simply using T? interface CustomProps<T = {}> { ...

The 'this' context setting function is not functioning as expected

Within my Vue component, I am currently working with the following code: import Vue from 'vue'; import { ElForm } from 'element-ui/types/form'; type Validator = ( this: typeof PasswordReset, rule: any, value: any, callback: ...

The name "Identifier" has already been declared before

I am currently working on a social network project to enhance my skills in nodejs and reactjs. While debugging the backend code for /signin using Postman, I encountered an error that prevents me from launching the node server. The error message displayed i ...

There is no 'next' property available

export function handleFiles(){ let files = retrieveFiles(); files.next(); } export function* retrieveFiles(){ for(var i=0;i<10;i++){ yield i; } } while experimenting with generators in T ...

Needing to utilize the provide() function individually for every service in RC4

In Beta, my bootstrapping code was running smoothly as shown below: bootstrap(App, [ provide(Http, { useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, helperService: HelperService, authProvider: AuthProvider) => new CustomHt ...

The function '() => Promise<T>' cannot be assigned to type 'Promise<T>'

Here is an interface I have: export interface ITreeViewItem { getChildren: Promise<ITreeViewItem[]>; ... Now, let's take a look at the implementation of it: export class MyClass implements ITreeViewItem { public async getChildren() ...

Angular data table is currently displaying an empty dataset with no information available

While attempting to display a data table in Angular JS, an issue arose where the table showed no available data despite there being 4 records present. Refer to the screenshot below for visual reference. This is the approach I took: user.component.ts imp ...

Angular - Is there a specific type for the @HostListener event that listens for scrolling on the window?

Encountering certain errors here: 'e.target' is possibly 'null'. Property 'scrollingElement' does not exist on type 'EventTarget'. What should be the designated type for the event parameter in the function onWindow ...

Why is it that I am limited to running globally installed packages only?

Recently, I made the switch to Mac iOS and encountered an issue while setting up a new TypeScript backend project. All npm packages seem to be not functioning properly in my scripts. Cannot find module 'typescript/bin/tsc' Require stack: - /Users ...

Create an interface that inherits from another in MUI

My custom interface for designing themes includes various properties such as colors, border radius, navbar settings, and typography styles. interface ThemeBase { colors: { [key: string]: Color; }; borderRadius: { base: string; mobile: st ...

Ways to utilize the scan operator for tallying emitted values from a null observable

I'm looking for an observable that will emit a count of how many times void values are emitted. const subject = new Subject<void>(); subject.pipe( scan((acc, curr) => acc + 1, 0) ).subscribe(count => console.log(count)); subject ...

What is the best way to create a function that shifts a musical note up or down by one semitone?

Currently developing a guitar tuning tool and facing some hurdles. Striving to create a function that can take a musical note, an octave, and a direction (up or down), then produce a transposed note by a half step based on the traditional piano layout (i. ...

In the realm of JavaScript and TypeScript, the task at hand is to locate '*' , '**' and '`' within a string and substitute them with <strong></strong> and <code></code>

As part of our string processing task, we are looking to apply formatting to text enclosed within '*' and '**' with <strong></strong>, and text surrounded by backticks with <code> </code>. I've implemented a ...

Exploring the Method of Utilizing JSON Attribute in typeScript

How to access JSON attributes in TypeScript while working on an Angular Project? I'm currently in the process of building an Angular project and I need to know how to access JSON attributes within TypeScript. test:string; response:any; w ...

Using 'cy.get' to locate elements in Cypress tutorial

Is there a way to search for one element, and if it's not found, search for another element? cy.get(@firstElement).or(@secondElement).click() Can I use a function similar to || in conditions for this scenario? ...

Type of Multiple TypeScript Variables

Within my React component props, I am receiving data of the same type but with different variables. Is there a way to define all the type variables in just one line? interface IcarouselProps { img1: string img2: string img3: string img4: string ...