I am writing an event system, comprised of EventTypes and EventListeners. I want to require that the listener methods implemented by an EventListener follow a certain expected structure. For example, with an event type 'foo' the function should be named onFoo and be compatible with (context: Context, data: EventDataType<'foo'>) => void | Promise<void>.
I am explicitly trying to move away from hardcoding the interface with the listener methods. My solution was to dynamically generate the functions' names via template literal types, and the expected data parameter type via generics. This works fine with type checking. But VS Code's IntelliSense does not recognize the structure of the dynamically generated interface, nor make suggestions for it, it only works with a hardcoded interface.
type EventType = 'start' | 'foo' | 'bar' | 'fooBar' | 'end';
type EventDataType<T extends EventType> =
T extends 'fooBar' ? { foo: string, bar: number }
: T extends 'foo' ? { foo: string }
: T extends 'bar' ? { bar: number }
: T extends EventType ? Record<string, any>
: never;
type ListenerFunction<T extends EventType> = (context: any, data: EventDataType<T>) => void | Promise<void>;
type EvtListenerMethods = {
[K in EventType as `on${Capitalize<K>}`]?: ListenerFunction<K>;
};
/*
* Dynamic solution attempt A: directly implement the generated type.
* Type checking works, but
* Intellisense does not suggest the methods.
*
* A.onBar will correctly raise a type error:
* Property 'onBar' in type 'A' is not assignable to the same property in base type 'EvtListenerMethods'.
* Type '(context: any, data: { abcd: any; }) => void' is not assignable to type 'ListenerFunction<"bar">'.
* Types of parameters 'data' and 'data' are incompatible.
* Property 'abcd' is missing in type '{ bar: number; }' but required in type '{ abcd: any; }'. ts(2416)
*/
class A implements EvtListenerMethods {
onFoo(context: any, data: EventDataType<'foo'>) { /*...*/ };
onBar(context: any, data: { abcd: any }) { /*...*/ };
}
/*
* Dynamic solution attempt B: implement an interface that extends the generated type.
* Type checking works, but
* Intellisense does not suggest the methods.
*
* B.onBar will correctly raise a type error:
* Property 'onBar' in type 'B' is not assignable to the same property in base type 'EvtListener'.
* Type '(context: any, data: { abcd: any; }) => void' is not assignable to type 'ListenerFunction<"bar">'.
* Types of parameters 'data' and 'data' are incompatible.
* Property 'abcd' is missing in type '{ bar: number; }' but required in type '{ abcd: any; }'. ts(2416)
*/
interface EvtListener extends EvtListenerMethods {
}
class B implements EvtListener {
onFoo(context: any, data: EventDataType<'foo'>) { /*...*/ };
onBar(context: any, data: { abcd: any }) { /*...*/ };
}
/*
* Hardcoded solution C: write out every method and its signature.
* Works both with type checking and with Intellisense suggestions.
*
* C.onBar will correctly raise a type error:
* Property 'onBar' in type 'C' is not assignable to the same property in base type 'EvtListenerHardcoded'.
* Type '(context: any, data: { abcd: any; }) => void' is not assignable to type 'ListenerFunction<"bar">'.
* Types of parameters 'data' and 'data' are incompatible.
* Property 'abcd' is missing in type '{ bar: number; }' but required in type '{ abcd: any; }'. ts(2416)
*/
interface EvtListenerHardcoded {
onStart?: ListenerFunction<"start">;
onFooBar?: ListenerFunction<"fooBar">;
onFoo?: ListenerFunction<"foo">;
onBar?: ListenerFunction<"bar">;
onEnd?: ListenerFunction<"end">;
}
class C implements EvtListenerHardcoded {
onFoo(context: any, data: EventDataType<'foo'>) { /*...*/ };
onBar(context: any, data: { abcd: any }) { /*...*/ };
}
As you can see in this screenshot, here is the behavior I get from IntelliSense for the hardcoded solution C which I am expecting to get from the dynamic solutions A and B:

If I make the methods required instead of optional, it does not change IntelliSense's suggestions. However, as expected, it does raise type errors stating that the classes are missing the required methods.
//...
type EvtListenerMethods = {
// Notice no optional "?"
[K in EventType as `on${Capitalize<K>}`]: ListenerFunction<K>;
};
//...
interface EvtListenerHardcoded {
// Notice no optional "?"
onStart: ListenerFunction<"start">;
onFooBar: ListenerFunction<"fooBar">;
onFoo: ListenerFunction<"foo">;
onBar: ListenerFunction<"bar">;
onEnd: ListenerFunction<"end">;
}
//...
Screenshot of (correctly expected) error raised when the class is not implementing all required methods from dynamically generated interface:
And yet no suggestions for the methods still:
How can I get IntelliSense to recognize the structure and make suggestions for the dynamically generated type EvtListenerMethods / interface EvtListener?

