Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How to define a TypeScript type with a variable number of generics

I’m attempting to make an emit function that can accept multiple arguments. Furthermore, the 2nd argument and beyond will be validated by TypeScript based on the 1st argument (the event).

I have the code below as a starting point, but it obviously doesn’t work 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 valid
emit('error', new Error('Ouch'), { count: 5 });

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

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>Solution :

In order to write handler(...args) once inside the body of emit() and have it compile with no errors, you need to make use of the technique introduced in microsoft/TypeScript#47109 as a fix for an issue I’ve called "correlated unions", as mentioned in microsoft/TypeScript#30581.

The first step is to come up with a type that maps your event values to the parameters list (the args array). It’s like your TrackablePlayerEventMap type, but the properties are just parameter tuple types and not function types:

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

Then we give eventHandlers type a type written explicitly in terms of TPEventParamsMap:

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

Conceptually that’s the exact same as your Partial<TrackablePlayerEventMap> type, but this one is explicitly written as a mapped type on TPEventParamsMap, which the compiler can use to keep track of the correlation between the event type (of type K) and the parameters to the event handler function (of type TPEventParamsMap[K]).

Continuing:

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

That compiles with no error. Note that since eventHandlers is Partial it might not have a value at key event, so we are using the optional chaining operator (?.) to call the function only if it exists.

And emit() behaves as desired from the caller’s side too:

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

Playground link to code

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading