Creating a TypeScript type that supports a flexible number of generic parameters

I am currently working on creating an emit function that has the capability to accept multiple arguments. In addition, TypeScript will validate the 2nd argument and beyond based on the 1st argument (the event).

The code provided below is a starting point, but it is not functioning as intended.

type CallbackFunc<T extends any[]> = (...args: T) => void;
interface TrackablePlayerEventMap {
    'play:other': CallbackFunc<['audio' | 'video']>;
    error: CallbackFunc<[Error, Record<string, unknown>]>;
}

const eventHandlers: Partial<TrackablePlayerEventMap> = {};

function on<K extends keyof TrackablePlayerEventMap>(event: K, callback: TrackablePlayerEventMap[K]) {
}

function emit<K extends keyof TrackablePlayerEventMap>(event: K, ...args: TrackablePlayerEventMap[K]) {
    const handler = eventHandlers[event];

    handler(...args);
}

on('error', (e, metadata) => {
    console.log(e, metadata)
});
on('play:other', x => {
    console.log(x);
});

// This should be considered valid
emit('error', new Error('Ouch'), { count: 5 });

// This should be deemed invalid
emit('error', 123, 456);

Answer №1

If you want to write handler(...args) just once in the body of emit() without any errors, you can apply the method mentioned in microsoft/TypeScript#47109 which addresses the issue known as "correlated unions" discussed in microsoft/TypeScript#30581.

The initial step involves creating a type that aligns your event values with the parameters list (the args array). This is similar to the TrackablePlayerEventMap, but here the properties are parameter tuple types instead of function types:

interface TPEventParamsMap {
    'play:other': ['audio' | 'video'],
    error: [Error, Record<string, unknown>];
}

Next, assign an explicit type for eventHandlers based on TPEventParamsMap:

const eventHandlers: {
    [K in keyof TPEventParamsMap]?: CallbackFunc<TPEventParamsMap[K]>
} = {};

In essence, this is equivalent to your

Partial<TrackablePlayerEventMap>
type, but explicitly defined as a mapped type on TPEventParamsMap, enabling the compiler to track the correlation between the event type (of type K) and the function parameters for the event handler (of type TPEventParamsMap[K]).

Continuing:

function emit<K extends keyof TPEventParamsMap>(
    event: K,
    ...args: TPEventParamsMap[K]
) {
    const handler = eventHandlers[event];
    handler?.(...args); // no errors
}

This code will compile without errors. Note that since eventHandlers is Partial, it may not have a value at key event, hence we utilize the optional chaining operator (?.) to invoke the function only if it exists.

From the caller's perspective, emit() will work as intended:

emit('error', new Error('Ouch'), { count: 5 }); // no issues
emit('error', 123, 456); // error, 123 is not an Error

Access the 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

What is the process of expanding types definitions by incorporating custom types?

I added these custom types to my project. The file structure can be found here. However, these types do not contain certain useful types such as ClientState. I want to include the following enum in those types: enum ClientState { DISCONNECTE ...

Having trouble dispatching a TypeScript action in a class-based component

I recently switched to using this boilerplate for my react project with TypeScript. I'm facing difficulty in configuring the correct type of actions as it was easier when I was working with JavaScript. Now, being new to TypeScript, I am struggling to ...

Tips for converting a date string to a date object and then back to a string in the same format

I seem to be encountering an issue with dates (shocker!), and I could really use some assistance. Allow me to outline the steps I have been taking. Side note: The "datepipe" mentioned here is actually the DatePipe library from Angular. var date = new Dat ...

Typescript is facing an issue locating the declaration file

I'm encountering an issue with TypeScript not recognizing my declaration file, even though it exists. Can anyone provide insight into why this might be happening? Here is the structure of my project: scr - main.ts - dec.d.ts str-utils - index. ...

Is there a method available that functions akin to document.getelementbyid() in this specific scenario?

Currently, I am tackling a project that involves implementing a search function. My initial step is to ensure that all input is converted to lowercase in order to simplify SQL calls. However, I have encountered a challenge that is proving difficult for me ...

Is there a way to stop TSC from performing type checking on projects within the node_modules directory

I'm encountering an issue where tsc is performing type-checking on files within the node_modules directory, resulting in errors like: > <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="513c287c21233e3b34322511617f617f ...

Angular - Executing a function in one component from another

Within my Angular-12 application, I have implemented two components: employee-detail and employee-edit. In the employee-detail.component.ts file: profileTemplate: boolean = false; contactTemplate: boolean = false; profileFunction() { this.profileTempla ...

Attempting to invoke a promise within a function yields an error message stating that it lacks call signatures

Recently, I came across this interesting class: export class ExponentialBackoffUtils { public static retry(promise: Promise<any>, maxRetries: number, onRetry?: Function) { function waitFor(milliseconds: number) { return new Pr ...

Utilizing TypeScript in a browser with a .NetCore WebApplication

After going through numerous articles, I have not been successful in finding a solution. My challenge lies with a .net core WebApplication that utilizes typescript code instead of javascript. Here are the specific requirements: I need to be able to debu ...

What is the best way to send ServerSideProps to a different page in Next.js using TypeScript?

import type { NextPage } from 'next' import Head from 'next/head' import Feed from './components/Feed'; import News from './components/News'; import Link from 'next/link'; import axios from 'axios&apo ...

Can someone assist me with running queries on the MongoDB collection using Node.js?

In my mongodb collection called "jobs," I have a document format that needs to display all documents matching my query. { "_id": { "$oid": "60a79952e8728be1609f3651" }, "title": "Full Stack Java Develo ...

Aligning arguments within the function signature

Is it possible to specify in TypeScript that the method 'foo' always expects FooData, etc. for the following code snippet: const search = (data: FooData|BarData|BazData, method:"foo"|"bar"|"baz") => { //Perform some common operations retu ...

Attach an event listener to a particular textarea element

Currently, I am developing a project in Next.js13 and my focus is on creating a custom textarea component. The goal is to have this component add an event listener to itself for auto-adjusting its height as the user types. Below is the relevant section of ...

Finding a solution to the dilemma of which comes first, the chicken or the egg, when it comes to using `tsc

My folder structure consists of: dist/ src/ In the src directory, I have my .ts files and in dist, I keep my .js files. (My tsconfig.json file specifies "outDir":"dist" and includes 'src'). Please note that 'dist' is listed in my git ...

Exploring for JSON keys to find corresponding objects in an array and adding them to the table

I'm currently working on a project where I need to extract specific objects from a JSON based on an array and then display this data in a table. Here's how my situation looks: playerIDs: number[] = [ 1000, 1002, 1004 ] The JSON data that I am t ...

Utilizing external clicks with Lit-Elements in your project

Currently, I am working on developing a custom dropdown web component using LitElements. In the process of implementing a feature that closes the dropdown when clicking outside of it, I have encountered some unexpected behavior that is hindering my progres ...

How can I configure React Router V6 to include multiple :id parameters in a Route path, with some being optional?

Currently, I am utilizing react-router@6 and have a Route that was previously used in V5. The route is for vehicles and always requires one parameter (:id = vehicle id), but it also has an optional second parameter (:date = string in DD-MM-YYYY format): &l ...

Trouble with storing data in Angular Reactive Form

During my work on a project involving reactive forms, I encountered an issue with form submission. I had implemented a 'Submit' button that should submit the form upon clicking. Additionally, there was an anchor tag that, when clicked, added new ...

What is the process of bringing in a Svelte component into a Typescript file?

Can a Svelte component be imported into a Typescript file and successfully compiled by Rollup? While the following code works fine as a Javascript file, it encounters errors when converted to Typescript, as the TS compiler struggles with a .svelte file: i ...

cookies cannot be obtained from ExecutionContext

I've been trying to obtain a cookie while working with the nestjs and graphql technologies. However, I encountered an issue when it came to validating the cookies by implementing graphql on the module and creating a UseGuard. It was suggested that I ...