What is the best way for a Promise to pass on the type of an overloaded function it invokes to the caller?

I created a custom EventEmitter subclass with function overloading to automatically type the callback when using addListener with a specific string:

const codes = {
    code1: 'code1',
    code2: 'code2',
} as const;

type CodeType = typeof codes;

interface Code1Interface { a: string };
interface Code2Interface { b: boolean };

class MyEmitter { // extends ...
    public addListener(code: CodeType['code1'], callback: (event: Code1Interface) => void): void;
    public addListener(code: CodeType['code2'], callback: (event: Code2Interface) => void): void;
    public addListener(code: CodeType[keyof CodeType], callback: (event: any) => void): void; // fallback
    public addListener(code: any, callback: any) {
        // call the parent addListener here
    }
}

Despite this setup, I encountered an issue where TypeScript couldn't infer the return type for a function that wraps addListener into a promise:

function getEvent(code: CodeType[keyof CodeType]) { // returns Promise<unknown>
    return new Promise((resolve, reject) => {
        const emitter = new MyEmitter();
        emitter.addListener(code, (event) => resolve(event));
    })
}

getEvent(codes.code1).then(x => x) // x has type unknown

I'm wondering if it's possible to pass on the interface type from the function overloading to the caller of getEvent or implement a more generic mapping approach that can be reused in the MyEmitter class.

demo

Answer №1

Overloads may not generalize effectively at the moment; they only match the correct call signature when directly called. When attempting to apply higher-order behavior, such as type inference, the compiler tends to default to the last call signature. Although this could change in the future (with a pending pull request at microsoft/TypeScript#52944 that would enhance this situation), currently managing overloads in TypeScript 5.1 can be challenging.

If we consider your code example, transitioning from overloads to generics might be a suitable choice:

type CodeValMap = {
    code1: Code1Interface;
    code2: Code2Interface;
    [k: string]: any;
}

class MyEmitter { 
    
    public addListener<K extends keyof CodeValMap>(
      code: K, callback: (event: CodeValMap[K]) => void
    ): void;

    public addListener(code: any, callback: any) {}
}

function getEvent<K extends keyof CodeValMap>(code: K) {
    return new Promise<CodeValMap[K]>((resolve, reject) => {
        const emitter = new MyEmitter();
        emitter.addListener(code, (event) => resolve(event));
    })
}

In this approach, we've established CodeValMap to map from specific string literal types like "code1" and "code2" to their respective event types, along with an optional index signature for fallback behavior if needed.

The generic implementation of MyEmitter's addListener allows for correlated code and callback arguments based on K extends keyof CodeValMap, while getEvent() follows a similar pattern by being a generic function considering K extends keyof CodeValMap, resulting in a Promise<CodeValMap[K]> return type which carries through the type of code:

getEvent(codes.c1).then(x => x) // (parameter) x: Code1Interface
getEvent(codes.c2).then(y => y) // (parameter) y: Code2Interface
getEvent("hoobydooby").then(z => z) // (parameter) z: any

This setup appears promising!


Note that it's plausible to dynamically generate CodeValMap using your existing codes constant and a manual type mapping from the values in codes to their corresponding interfaces, as illustrated below:

const codes = {
    c1: 'code1',
    c2: 'code2',
} as const;

type CodeType = typeof codes;

interface CodeKeyMap {
    c1: Code1Interface,
    c2: Code2Interface
}

type CodeValMap = {
    [K in keyof CodeType as CodeType[K]]: CodeKeyMap[K]
} & { [k: string]: any }

This programmatically generated CodeValMap serves the same purpose as manually crafted ones, offering the advantage of maintaining key-oriented code rather than focusing on values from codes.

Playground link to code

Answer №2

To ensure proper functioning, it is necessary to specify the generic parameter Promise<T> rather than relying on inference from callback functions.
It is advisable to utilize a type-only library like typed-emitter for accurate event emitter types.

interface Code1Interface { a: string };
interface Code2Interface { b: boolean };

import TypedEmitter from 'typed-emitter'
import EventEmitter from 'node:events'

type MyEvents = {
    code1: (event: Code1Interface) => void
    code2: (event: Code1Interface) => void
}

class MyEmitter extends (EventEmitter as new () => TypedEmitter<MyEvents>) {}

function getEvent<Code extends keyof MyEvents>(code: Code) {
    return new Promise<Parameters<MyEvents[Code]>[0]>((resolve, reject) => {
        const emitter = new MyEmitter();
        emitter.addListener(code, (event) => resolve(event));
    })
}
let x = await getEvent('code1')
//  ^?
// let x: Code1Interface

// Another way is to place it into the return statement
function getEvent2<Code extends keyof MyEvents>(code: Code): Promise<Parameters<MyEvents[Code]>[0]> {
    return new Promise((resolve, reject) => {
        const emitter = new MyEmitter();
        emitter.addListener(code, (event) => resolve(event));
    })
}

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 are some strategies to avoid losing form data while switching between components without refreshing the page?

I am looking for a solution to seamlessly swap between two components, each containing a form, while retaining the data inputted by the user without submitting. This functionality is similar to switching tabs in simple HTML. My approach involves using a v ...

Creating a clickable div in Angular 14, similar to the Gmail webpage, involves setting up a div element that triggers an event when clicked. When the div is clicked, it should display an array of data

Recently, I created an inbox component nested under the main page's routing. Before that, my goal is to develop a page resembling the functionality of the Gmail web app. In order to achieve this, I am looking to create a trio of clickable div boxes. ...

Retrieve the getIdToken from the firebase.User object using TypeScript

import { AngularFireAuth } from 'angularfire2/auth'; import * as firebase from 'firebase/app'; @Injectable() export class GetRequestsProvider { user: firebase.User; userIdToken:string; constructor(public http: HttpClient, publ ...

Service function in Angular 2 is returning an undefined value

There are two services in my project, namely AuthService and AuthRedirectService. The AuthService utilizes the Http service to fetch simple data {"status": 4} from the server and then returns the status number by calling response.json().status. On the ot ...

What is the best way to retrieve a variable from a ReaderTaskEither within an error handling function using fp-ts?

I'm facing a challenge with my code that involves the usage of ReaderTaskEither: export const AddUserToTeam = ({ userId, teamId }: AddUserToTeamDto) => { return pipe( // ...

TypeScript and Node.js: The type of 'this' is implicitly set to 'any'

Help Needed with TypeScript issue: An issue is arising in my TypeScript code related to a mongoose schema's post function. It is used to generate a profile for a user upon signing up, using the User model. Even though the code functions properly, th ...

Circular dependency in typescript caused by decorator circular reference

Dealing with a circular dependency issue in my decorators, where the class ThingA has a relation with ThingB and vice versa is causing problems for me. I've looked into various solutions that others have proposed: Beautiful fix for circular depende ...

Controlling the Output in Typescript without Restricting the Input

I am interested in passing a function as a parameter, allowing the function to have any number of parameters but restricting the return type to boolean. For example, a function Car(driver: Function) could be defined where driver can be ()=>boolean or ( ...

What is the best way to implement a hover effect on multiple rows within an HTML table using Angular?

I am currently working on developing a table preview feature to display events. I previously sought assistance here regarding positioning elements within the table and successfully resolved that issue. Applying the same principles, I am now attempting to c ...

Transform a Promise of string array into a regular string array

I have a function called fetchSavedCards that retrieves a list of saved cards. The function returns a Promise Object, but I want to convert it into an array of strings and return it. Is this possible? const fetchSavedCards = (): string[] => { return fe ...

Converting unknown attributes into choices that can be omitted

I am currently working on creating a mapped type that will turn certain properties into optional ones if they can be undefined. To elaborate, my starting point is a type structured like this: interface Input { foo: string; bar: string | undefined; b ...

A step-by-step guide on incorporating MarkerClusterer into a google-map-react component

I am looking to integrate MarkerClusterer into my Google Map using a library or component. Here is a snippet of my current code. Can anyone provide guidance on how I can achieve this with the google-map-react library? Thank you. const handleApiLoaded = ({ ...

Issue with using "tabindex" in IE 11 - needing to press the tab key twice to move to the next input in an Angular 7 project

While this code functions correctly in Chrome and other browsers, it encounters issues when run in Internet Explorer. Even with the addition of tabindex=1, the desired output is not achieved in IE browsers. <div> <ul> <ng-container ...

Exploring the incorporation of interfaces into Vue.js/Typescript for variables. Tips?

I have an interface:   export interface TaskInterface{ name: string description1: string time: string } and a component import { TaskInterface } from '@/types/task.interface' data () { return { tasks: [ { name: 'Create ...

Convert an object to an array, but only if it is not already an array

If we need to iterate over either an Object or an Array of Objects, we can transform the Object into an array of one object and then iterate in our React App accordingly. Let's consider an example: // Returned value as object const zoo = { lion: &a ...

Can you provide guidance on defining functions using standard syntax and incorporating children in React with TypeScript?

There are multiple ways to type it, such as using the interface React.FC<YourInterface> or explicitly declaring in an interface the type of children as JSX.Element or React.Node. Currently, my approach is: const MyComponent: React.FC<MyInterface& ...

Angular offers pre-determined values that cannot be altered, known as "

I am currently learning Angular and TypeScript, and I came across a task where I need to create an object or something similar that allows me to define a readable but not editable attribute. In Java, I would have achieved this by doing the following: publ ...

Unable to find the module... designated for one of my packages

Within my codebase, I am utilizing a specific NPM package called my-dependency-package, which contains the module lib/utils/list-utils. Moreover, I have another package named my-package that relies on my-dependency-package. When attempting to build the pr ...

Retrieve information from an XML document

I have some XML content that looks like this: <Artificial name="Artifical name"> <Machine> <MachineEnvironment uri="environment" /> </Machine> <Mobile>taken phone, test when r1 100m ...

Error: The "require" function is undefined and cannot be recognized in app.js on line 3

Encountering difficulties in connecting front-end HTML to a private blockchain for interacting with a smart contract. Steps completed thus far: Created a smart contract and deployed it on the private blockchain. npm install -g web3 Developed an HTML fil ...