core icon indicating copy to clipboard operation
core copied to clipboard

Improve DefineComponent typings to support precise event mapping for consuming Custom Elements

Open JulianCataldo opened this issue 6 months ago • 1 comments

Vue version

3.x

Link to minimal reproduction

https://play.vuejs.org/#eNqVU0uP2jAQ/ivTXLIrsXBoT2lA2m7pa6W2Kkg9YA4mmQQvjm3ZDgWh/PeOnSVQqVppD3l45vPkezin5N6Y8b7FJEtyV1hhPDj0rQHJVT1liXcsmTElGqOthxNYrKCDyuoGUtqWMsVUoZXz0LgapqF/k35BKTX81laWb9LbAJlMYIEIvNkIVH5cjmluKN5Lp0fgt8KB46rc6AMsFyCcSj0YUeyEqoHI+C1CLfWGS+Bt3dAI7oVWkE2Yyic9b2JJC4+NkdwjrQDy5nhXCi51DZmx2pCeDbckKJ8MHQLmk6tdyYg0k6JK1OMnpxUZcwqzWFLoxgiJ9ocJ3yZfMoid0OMk+M+3WPO2xdG5Xmyx2P2n/uQOocaSnxYd2j2yZOh5bmv0fXu++I4Heh+ajS5bSegXmr/QadkGjj3sQ6tKon2Fi2y/xkjJ4KWbHzwqdxYViAZkF/EsoZgfXpB+oft2/C7uY6ojF6+zJhP90SA8tM7rZi4xROjouMQp6RBGOszFfUAMSwL1iDttUBHseVJA5ScIxQw2WkvkCrrZ+35XF5/hHi6mIod5I7z7RAc4X87ODFaPIBTs8KgrWK7S/uPpep3BDWbXldXj+hamM9hrUT6PhTC5xEJyi9An0P8aYTQjgz3aihcIn+MBDlZqFeVfPBwMCDZ+xEqoaHnE5WcHTuEIh0ArrVnSpxPrr3i9iP83itVVBuuL3PgXAcRHjLX7C4T9Yfc=

Steps to reproduce

  1. Create a CE registry like:
type CustomElements = {
  'my-dialog': {
    events: {
      'dialog-open': CustomEvent<{ open: boolean }>;
    };
  };
};

type EmitsFrom<T> = {
  [K in keyof T['events']]: (e: T['events'][K]) => void;
};
  1. Try to create a wrapper type:
type EmitsFrom<T> = {
  [K in keyof T['events']]: (e: T['events'][K]) => void;
};
  1. Use it in DefineComponent:
DefineComponent<..., ..., ..., ..., ..., ..., ..., EmitsFrom<CustomElements['my-dialog']>>;
  1. Get a TS error because mapped types like { [K in keyof]: Fn } are not assignable to Vue’s Record<string, Fn | null> emits constraint. It's in the runtime core dts:
export type ObjectEmitsOptions = Record<string, ((...args: any[]) => any) | null>;
export type EmitsOptions = ObjectEmitsOptions | string[];

What is expected?

Vue’s DefineComponent should support mapped types in the emits slot so developers can preserve key-specific event typing (especially important when consuming pre-typed Custom Elements from a registry).

What is actually happening?

Due to the strict Record<string, Fn | null> constraint in EmitsOptions, generic mapped types are rejected, forcing developers to collapse their union (losing key-specific event typing).

This limits Vue’s TypeScript ergonomics when integrating Custom Elements or other external type registries.

Any additional comments?

TS in the sandbox doesn't seems to pick ambient typings.

JulianCataldo avatar Jun 23 '25 10:06 JulianCataldo

as a workaround: Playground

jh-leong avatar Jun 24 '25 06:06 jh-leong

Cool, thank you very much! Seems easier that way, versus messing with the 8th generic dedicated to "real" Vue emits. Basically onFooBar in Props generic get transformed to @foo-bar. So I need to pretransform real DOM event names like foo-bar to onFooBar first 😅 Then Vue does @foo-bar back. I guess there is a benefit to gain from this dance: maybe it will make Vue JSX support a free passenger.

JulianCataldo avatar Jun 28 '25 15:06 JulianCataldo