Determining the type of an overloaded method within a generic function

I am currently working on developing a versatile function that can subscribe to an event emitter. The function subscribe is designed to take 3 arguments: event name, event handler, and the event emitter to connect to.

I am looking for ways to ensure accurate type definitions inference, especially for the event handler. This way, when I provide, for example, the process object as an event emitter and 'warning' as the event name, I won't need to explicitly specify the types of handler arguments:

subscribe('warning', (warning) => {/* */}, process);
//                    ^
//                    the `warning` argument should have `Error` type as defined 
//                    in `process` interface:
//
//                    interface Process {
//                      ...
//                      addListener(event: "warning", listener: WarningListener): this;
//                    }
//                    ...
//
//                    type WarningListener = (warning: Error) => void;

Below is my implementation along with an example usage. However, it only seems to function properly with the second event ('b'), and not with event 'a'.

interface Target<N, H> {
  on(name: N, handler: H): unknown;
}

const subscribe = <
  N extends T extends Target<infer N, unknown> ? N : never,
  H extends T extends Target<N, infer H> ? H : never,
  T extends Target<unknown, unknown>,
>(
  name: N,
  handler: H,
  target: T
) => {
  target.on(name, handler);
};

const target: {
  on(name: 'a', handler: (a1: string) => void): void;
  on(name: 'b', handler: (b1: number) => void): void;
} = {
  on: ()=>{},
};

// errors with: Argument of type '"a"' is not assignable to parameter of type '"b"'.
subscribe('a', (a1) => console.log(a1), target);

// works fine
subscribe('b', (b1) => console.log(b1), target); 

Answer №1

Thanks to the assistance of @jcalz, I was able to uncover the solution to my issue.

Regrettably, the documentation explicitly states that it is not feasible to fully deduce types for the overloaded methods using `subscribe`, which means it won't be compatible with certain built-in event targets like `process.on('warning', ...)`. However, there's a workaround: by catering to targets that adhere to a specific interface as demonstrated below:

interface Target<E extends Record<string, (...args: any[]) => any>> {
  on<N extends Extract<keyof E, string>>(name: N, handler: E[N]): void;
}

Subsequently, we can craft the `subscribe` function as follows:

const subscribe = <
    EventMap extends Record<string, (...args: any[]) => any>,
    Name extends Extract<keyof EventMap, string>,
  >(
    name: Name,
    handler: EventMap[Name],
    target: Target<EventMap>
  ) => { target.on(name, handler); };

For additional flexibility, why not transform `Target` into a class so other users can leverage its functionality without reinventing the wheel:

import { EventEmitter } from 'events';

class Target<E extends Record<string, (...args: any[]) => any>> {
  private _map: E | undefined;
  private _eventEmitter = new EventEmitter();
  on<N extends Extract<keyof E, string>>(name: N, handler: E[N]): void {
    this._eventEmitter.on(name, handler)
  }
}

Below you can find the complete example code snippet:

import { EventEmitter } from 'events';

const subscribe = <
    EventMap extends Record<string, (...args: any[]) => any>,
    Name extends Extract<keyof EventMap, string>,
  >(
    name: Name,
    handler: EventMap[Name],
    target: Target<EventMap>
  ) => { target.on(name, handler); };

class Target<E extends Record<string, (...args: any[]) => any>> {
  private _map: E | undefined;
  private _eventEmitter = new EventEmitter();
  on<N extends Extract<keyof E, string>>(name: N, handler: E[N]): void {
    this._eventEmitter.on(name, handler)
  }
}

type TestEventMap = {
  a: (a1: string) => void;
  b: (b1: number) => void;
}

class Test extends Target<TestEventMap> {}

const test = new Test();

subscribe('a', (a1) => console.log(a1), test);
//              ^
//              inferred type: string
subscribe('b', (b1) => console.log(b1), test); 
//              ^
//              inferred type: number

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

Jasmine was unsuccessful in detecting a exported function being invoked by another function

In my code, I've created 2 helper functions where one is a shortcut to the other. I need to verify in my test that the shortcut function is actually calling the main function. Both functions are located in the same file: export function test1(param1, ...

Incorporating a CSS Module into a conditional statement

Consider the following HTML structure <div className={ `${style.cell} ${cell === Player.Black ? "black" : cell === Player.White ? "white" : ""}`} key={colIndex}/> Along with the associated CSS styles .cell { ...

Is it possible to simultaneously update two entities using a single endpoint?

In order to update data in two different entities with a @OneToOne relationship between UserEntity and DetailsEntity, I need to create a function in my service that interacts with the database. Here are the entity definitions: UserEntity @Entity() export ...

What is causing this error/bug to show up in Angular?

I encountered an error while working on my Angular project that incorporates both front-end and back-end development with Python Flask. Even though the page updates correctly, a database-related error is being displayed in the console. Below are the snippe ...

Utilizing TypeScript: Importing a typed module within a d.ts file (from an npm package)

I am currently working on integrating a definition file into an npm package that has dependencies on React. The specific library in question can be found at https://github.com/eiriklv/react-masonry-component. In my TypeScript project, I have successfully ...

Sorting an array of objects in TypeScript may result in a type error

My data includes various categories, ages, and countries... const data = [ { category: 'Fish', age: 10, country: 'United Kingdom' }, { category: 'Fish', age: 9, country: 'United Kingdom' }, { category: ...

Leveraging Shared Modules Component across multiple modules in Angular can enhance code re

In my project structure, I have a shared folder containing shared.module.ts. Additionally, there is a modules folder with sub-modules, one of which is Dashboard.module.ts. Inside the shared module, I created a custom sidebar menu that I intend to use withi ...

Showing the Enum name rather than the value in an Angular HTML template for a bound Typescript Interface

After retrieving an array of objects with various properties from a .NET Controller, I am utilizing the following Typescript code: import { Component, Inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Co ...

Is there a way to convert a JavaScript object to a class while eliminating unnecessary properties?

In my current project, I am using Typescript with ExpressJS to build APIs. Let's say I have a typescript User model defined as follows: class UserModel { id: number; email: string; password: string; name: string; dob: Date; ge ...

Do Prisma Migrations Require a Default Value?

I'm struggling with Prisma data modeling and have tried almost everything to resolve an error I keep getting. The error states that the table needs a default value even though I have already assigned it an ID. I attempted to remove the relation name, ...

Using array values with styled-components may lead to an object being potentially 'undefined'

Currently, I am developing a custom Skeleton component that allows for the input of a property called circleSizes. This property is an array of numbers used to define the width and height of the Skeleton element. Below is the code snippet for the componen ...

Angular - enabling scroll position restoration for a singular route

Is there a way to turn off scroll restoration on a specific page? Let's say I have the following routes in my app-routing.module.ts file... const appRoutes: Routes = [{ path: 'home', component: myComponent}, { path: 'about', compon ...

Ways to verify the input label in Angular version 4 and above?

I'm working on an Angular component that includes a form structured like this: <form> <label for="Name">Click me</label> <input type="text" id="Name" name="Name" /> <label for="Name2">Click me 2</label> &l ...

Tips for implementing a personalized Circuit Breaker in Node.js that monitors request volume

Currently, I am exploring the most effective way to implement a circuit breaker based on the number of requests served in a Typescript/express application rather than fail percentage. Given that the application is expected to accommodate a large volume of ...

Put the Toastr notifications inside a designated container within a particular component

Toastify allows you to set a global container for displaying toasts using the following method: import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from ...

Implement a personalized Laravel Dusk selector with the attribute data-dusk

In the world of Laravel Dusk, the default selector hunts for the dusk="something" attribute in your HTML. If you want to dive deeper into this topic, check out this resource. However, when it comes to compatibility with Typescript for React/Vue, ...

When I attempt to conceal the filter within mat-table using *ngIf, I encounter an issue where I am unable to read the property 'value' due to it being

After creating a mat-table, I encountered an issue when trying to show and hide my input filter area using a button. If I use *ngIf="showInputFilter" to hide the filter area, I receive the error message Cannot read property 'value' of u ...

What could be causing the issue with my React Native app's release version not opening on a physical Android device?

I encountered an issue while trying to install the Release version of my application. In order to test both the debug and release versions on my physical and virtual devices, I utilized the following commands: ./gradlew assembleDebug, ./gradlew assembleRel ...

Tips for extracting specific JSON response data from an array in TypeScript

I have an array named ReservationResponse, which represents a successful response retrieved from an API call. The code snippet below demonstrates how it is fetched: const ReservationResponse = await this.service.getReservation(this.username.value); The st ...

Unable to locate 'reflect-metadata' module within Docker container on Production Server

I encountered an error: module.js:550 throw err; ^ Error: Cannot find module 'reflect-metadata' at Function.Module._resolveFilename (module.js:548:15) at Function.Module._load (module.js:475:25) at Module.require ( ...