core icon indicating copy to clipboard operation
core copied to clipboard

[TypeScript] Allow HTML attributes to Vue component with type, if an attribute has the same name with the component's prop, keep the prop type instead of intersection types

Open otomad opened this issue 2 years ago • 6 comments

Vue version

3.3.4

Link to minimal reproduction

https://stackblitz.com/edit/vue3-typescript-vue-cli-starter-7pb4bu?file=src%2Fcomponents%2FHelloWorld.vue,src%2Fvue.d.ts,src%2FApp.vue

Steps to reproduce

The Vue component cannot accept HTML attributes with type in TypeScript, or get any type.

It won't get suggestion of HTML attributes when typing in a component props. When typing a native HTML event in component, its parameters will get any type, so TypeScript will report an error.

The API shows that we can extends the props for all components with types.

Just create a .d.ts file in the project.

types/vue.d.ts

declare module "vue" {
    export interface AllowedComponentProps extends HTMLAttributes {}
}

Then all components will accept HTML attributes in TypeScript.

If I create a component:

components/MyComponent.vue

<script setup lang="ts">
    defineProps<{
        id: number;
    }>();

    defineEmits<{
        click: [num: number];
    }>();
</script>

And use it with:

<MyComponent
    :id="1"
    lang="en"
    @click="num => handleNumber(num)"
    @mouseenter="e => handleMouseEvent(e)"
/>

What is expected?

The id prop is defined in MyComponent, so it will use the prop type number instead of HTML attribute id type string;

The lang prop isn't defined in MyComponent, so it will use HTML attribute lang type string instead of unknown or any.

The click event is defined in MyComponent, so it will use the emit type (num: number) => void instead of HTML attribute onClick type (payload: MouseEvent) => void;

The mouseenter event isn't defined in MyComponent, so it will use HTML attribute onMouseenter type (payload: MouseEvent) => void instead of unknown or any.

What is actually happening?

Without interface AllowedComponentProps extends HTMLAttributes declaration

Prop or emit Actually type Expected type Notes
id number number
lang any string
click (num: number) => void (num: number) => void
mouseenter any (payload: MouseEvent) => void Get TypeScript error:
Parameter e implicitly has an any type

With interface AllowedComponentProps extends HTMLAttributes declaration

Prop or emit Actually type Expected type Notes
id never number Trying to merge number & string so that get never
lang string string
click (((num: number) => any) & ((payload: MouseEvent) => void)) (e: number) => void The parameter num or payload will get never type
mouseenter (payload: MouseEvent) => void (payload: MouseEvent) => void

System Info

System:
    OS: Windows 10 10.0.22621
    CPU: (8) x64 Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
    Memory: 1.30 GB / 11.80 GB
  Binaries:
    Node: 19.0.1 - D:\Program Files\nodejs\node.EXE
    Yarn: 1.22.17 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 8.19.2 - D:\Program Files\nodejs\npm.CMD
    pnpm: 7.26.3 - ~\AppData\Roaming\npm\pnpm.CMD
  Browsers:
    Edge: Spartan (44.22621.1702.0), Chromium (115.0.1901.203)
    Internet Explorer: 11.0.22621.1

Any additional comments?

No response

otomad avatar Aug 16 '23 03:08 otomad

import type { ButtonHTMLAttributes } from 'vue';

Shyam-Chen avatar Aug 17 '23 02:08 Shyam-Chen

ButtonHTMLAttributes

I am just using the button as an example and should actually support all elements.

otomad avatar Aug 20 '23 06:08 otomad

I found the solution.

In file @vue/runtime-core/dist/runtime-core.d.ts, find the type declaration of ComponentPublicInstance, then change its property $props from

$props: Prettify<MakeDefaultsOptional extends true ? Partial<Defaults> & Omit<P & PublicProps, keyof Defaults> : P & PublicProps>;

to

$props: Prettify<MakeDefaultsOptional extends true ? Partial<Defaults> & Omit<Override<PublicProps, P>, keyof Defaults> : Override<PublicProps, P>>;

Where type Override is

type Override<T, U> = Omit<T, keyof U> & U;

otomad avatar Sep 02 '23 06:09 otomad

I found the solution.

In file @vue/runtime-core/dist/runtime-core.d.ts, find the type declaration of ComponentPublicInstance, then change its property $props from

$props: Prettify<MakeDefaultsOptional extends true ? Partial<Defaults> & Omit<P & PublicProps, keyof Defaults> : P & PublicProps>;

to

$props: Prettify<MakeDefaultsOptional extends true ? Partial<Defaults> & Omit<Override<PublicProps, P>, keyof Defaults> : Override<PublicProps, P>>;

Where type Override is

type Override<T, U> = Omit<T, keyof U> & U;

@otomad wow, LGTM. Looking forward to your PR for this issue.

rudyxu1102 avatar Sep 02 '23 11:09 rudyxu1102

You can create vue.d.ts with following content:

import Vue from 'vue'

declare module 'vue' {
  export interface AllowedComponentProps extends HTMLAttributes {
    onClick?: ((payload: MouseEvent) => void) | undefined
  }

  export default Vue
}

From my blog: https://blog.luoling.moe/2024/07/29/vue-declare/ btw, it takes my 1 hour to find the solution...

luoling8192 avatar Jul 29 '24 18:07 luoling8192

By the way, for any friends who have found their way here, if you need to make it possible to use both className and class in Vue TSX, you can also use declare module.

/**
 * Add className support for html/svg attributes in Vue components
 * @example
 *   <div className="leading-[22px]">
 *            ^~ The purpose is to support this pattern (legacy React syntax)
 *       <span class="inline-flex">
 *                 ^~ jsxImportSource==vue, for Vue JSX syntax
 *         ...
 *       </span>
 *   </div>
 */
declare module 'vue' {
  interface HTMLAttributes {
    className?: any;
  }
  interface SVGAttributes {
    className?: any;
  }
}

eric-gitta-moore avatar Nov 11 '25 12:11 eric-gitta-moore