1

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: VS Code Intellisense suggesting the unimplemented methods from the hardcoded interface

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:

VS Code TS type checking raises error on class not implementing all required methods from dynamically generated interface that it extends

And yet no suggestions for the methods still:

VS Code IntelliSense not suggesting the unimplemented methods from the dynamically generated interface

How can I get IntelliSense to recognize the structure and make suggestions for the dynamically generated type EvtListenerMethods / interface EvtListener?

6
  • 1
    This is interesting. I'm not sure why autosuggest doesn't work for key-remapped mapped types like this. I haven't found a relevant github issue yet either. Commented Oct 28 at 19:24
  • My project's typescript version was 5.9.2, updated to the latest 5.9.3 and it still behaves the same. My VS Code version is July 2025 (1.103.2). I see that there is a September 2025 (1.105) update available, I will work on upgrading VS Code. I run a portable VS Code installation on this computer/network so it doesn't automatically update itself. Commented Oct 28 at 20:01
  • I doubt this has anything to do with Visual Studio directly. TS is responsible for what suggestions are provided to an IDE, so I'd think the language is the issue, not the IDE. I still haven't found anything in github issues so it's possible someone (you?) might want to file this as a feature request and/or bug report. Commented Oct 28 at 20:06
  • That makes a lot of sense. Thank you. I will report it to github.com/microsoft/TypeScript/issues similar to what I documented here. (I assume that is the correct repo as I have seen other issues in there regarding Intellisense too.) Commented Oct 28 at 20:30
  • 2
    for completeness and status tracking on this bug(?), here is a link to the issue I submitted. github.com/microsoft/TypeScript/issues/62689 Commented Oct 29 at 13:44

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.