Using TSX writing method in Vue3 can enjoy the intelligent hints brought by TX, improve code standardization and development efficiency, but strict type checking will also bring us some trouble, for example, listening for events in custom components will report type errors.

Note: Vue version number is 3.0.0

Error scenario – Native event

Anyone with a basic understanding of Vue3 will be happy to encapsulate a simple component in template and use it:

// ClickMe.vue
<template>
  <div>{{ title }}</div>
</template>

<script lang="ts">
export default {
  name: 'Click Me'.props: {
    title: {
      type: String.required: true}}}</script>

// App.vue
<template>
  <div>
    <ClickMe title="点我" @click="handleClick" />
  </div>
</template>

<script lang="ts">
import ClickMe from '@/components/ClickMe.vue'

export default {
  name: 'App'.components: {
    ClickMe
  },
  setup () {
    const handleClick = () = > {
      console.log('Hello World')}return {
      handleClick
    }
  }
}
</script>
Copy the code

Now change the code from template to TSX and the ClickMe component changes smoothly:

// ClickMe.tsx
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Click Me'.props: {
    title: {
      type: String.required: true
    }
  },
  setup (props) {
    return () = > (
      <div>{ props.title }</div>)}})Copy the code

App.vue = app.tsx = app.tsx = app.tsx = app.tsx = app.tsx = app.tsx = app.tsx

// App.tsx
import ClickMe from '@/components/ClickMe'

export default {
  name: 'App',
  setup () {
    const handleClick = () = > {
      console.log('Hello World')}return () = > (
      <div>
        <ClickMe title="点我" onClick={ handleClick} / >
      </div>)}}Copy the code

because the ClickMe component does not have the onClick prop.

Native Events – Solution one

The TSX will only type check components that use PascalCase. If we register the component globally or locally within the component, it will be OK to use kebab-case in TSX.

// App.tsx
import ClickMe from '@/components/ClickMe'

export default {
  name: 'App'.// Local registration
  components: {
    ClickMe
  },
  setup () {
    const handleClick = () = > {
      console.log('Hello World')}return () = > (
      <div>
        <click-me title="点我" onClick={ handleClick} / >
      </div>)}}Copy the code

Native Events – Solution two

The first solution is to solve the problem, but the type check and intelligent hints brought by TS are also gone, which makes me feel like I have practiced seven injured fists. This approach may be more appropriate for third-party components that reference global components or whose props type is unclear. There is a more elegant solution for custom local components.

Extract the Props

> > props > props > props > props > props > props

// ClickMe.tsx
import { defineComponent } from 'vue'

const ClickMePropsDefine = {
  title: {
    type: String.required: true}}as const

export default defineComponent({
  name: 'Click Me'.props: ClickMePropsDefine,
  setup (props) {
    return () = > (
      <div>{ props.title }</div>)}})Copy the code

Type declaration

Declare a common component base Props definition:

// @/types/index.ts
export const BasePropsDefine = {
  onClick: Function
  // You can add other native event declarations here
} as const
Copy the code

Component Type Declaration

Combine the props type definition and declare a new component type:

// ClickMe.tsx
import { BasePropsDefine } from '@/types'

constPropsDefineWrapper = { ... BasePropsDefine,// The props definition is written in front of the props definition so that custom props can be overridden. ClickMePropsDefine }as const

type ClickMeType = DefineComponent<typeof PropsDefineWrapper>
Copy the code

The export component

Display the declared component type before exporting it

// ClickMe.tsx
const ClickMe: ClickMeType = defineComponent({
  name: 'Click Me'.props: ClickMePropsDefine,
  setup (props) {
    return () = > (
      <div>{ props.title }</div>)}})export default ClickMe
Copy the code

Remove the local component registration from app. TSX and replace the ClickMe component with PascalCase. Find that the type errors are gone and the ts type checking and intelligent prompt are left.

// App.tsx
import ClickMe from '@/components/ClickMe'

export default {
  name: 'App',
  setup () {
    const handleClick = () = > {
      console.log('Hello World')}return () = > (
      <div>
        <ClickMe title="点我" onClick={ handleClick} / >
      </div>)}}Copy the code

Native Events – Solution 3

Solution 2 makes up for solution 1, but it’s a fly in the ointment because the code is too tedious, with a lot of repetitive logic every time you write a custom component. As a programmer, of course, you need to extract the repetitive logic and encapsulate it into a public function.

Source code analysis

Let’s start by looking at the vue3 source code declaration for the defineComponent function:

export declare function defineComponent<Props.RawBindings = object> (setup: (props: Readonly<Props>, ctx: SetupContext) => RawBindings | RenderFunction) :DefineComponent<Props.RawBindings>;

export declare function defineComponent<Props = {}, RawBindings = {}, D = {}, C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = EmitsOptions, EE extends string = string>(options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>;

export declare function defineComponent<PropNames extends string.RawBindings.D.C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = Record<string.any>, EE extends string = string>(options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<Readonly<{
    [key inPropNames]? :any;
}>, RawBindings, D, C, M, Mixin, Extends, E, EE>;

export declare function defineComponent<PropsOptions extends Readonly<ComponentPropsOptions>, RawBindings.D.C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = Record<string.any>, EE extends string = string>(options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>;
Copy the code

The defineComponent function specifies four overloaded types. The first overloaded type is

Basic Props type declaration

First change BasePropsDefine to a type declaration

// @/types/index.ts
export type BasePropsDefine = {
  readonly onClick: FunctionConstructor;
}
Copy the code

Function declaration

Next, define a defineTypeComponent function and its two overload types:

import { BasePropsDefine } from '@/types'
import { ComponentOptionsMixin, ComponentOptionsWithObjectProps, ComponentOptionsWithoutProps, ComponentPropsOptions, ComputedOptions, DefineComponent, defineComponent, EmitsOptions, MethodOptions } from 'vue'

// The component has no custom Props
export function defineTypeComponent<Props = {}, RawBindings = {}, D = {}, C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = EmitsOptions, EE extends string = string>(options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<BasePropsDefine, RawBindings, D, C, M, Mixin, Extends, E, EE>;
// An overloaded type for a component with custom Props
export function defineTypeComponent<PropsOptions extends Readonly<ComponentPropsOptions>, RawBindings.D.C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = Record<string.any>, EE extends string = string>(options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<BasePropsDefine & PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>;
// Call defineComponent directly
export function defineTypeComponent (options: any) {
  return defineComponent(options)
}
Copy the code

Define BasePropsDefine (BasePropsDefine); define BasePropsDefine (BasePropsDefine);

// The return type of the component does not have custom Props
DefineComponent<BasePropsDefine, RawBindings, D, C, M, Mixin, Extends, E, EE>;
// The return type for a component that has custom Props
DefineComponent<BasePropsDefine & PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>;
Copy the code

Used by the Props component

The newly defined methods introduced in Clickme.tsx are:

// ClickMe.tsx
import { defineTypeComponent } from '@/utils'

const ClickMePropsDefine = {
  title: {
    type: String.required: true}}as const

export default defineTypeComponent({
  name: 'Click Me'.props: ClickMePropsDefine,
  setup (props) {
    return () = > (
      <div>{ props.title }</div>)}})Copy the code

No errors are reported, and type checking and intelligence are in place.

None Used by the Props component

Try removing the prop again:

// ClickMe.tsx
import { defineTypeComponent } from '@/utils'

export default defineTypeComponent({
  name: 'Click Me'.// props: ClickMePropsDefine,
  setup (props) {
    return () = > (
      <div>Am I</div>)}})// App.tsx
import ClickMe from '@/components/ClickMe'

export default {
  name: 'App',
  setup () {
    const handleClick = () = > {
      console.log('Hello World')}return () = > (
      <div>
        <ClickMe onClick={ handleClick} / >
      </div>)}}Copy the code

There is no problem, that the two overload methods are in effect, the job is done!

Extension – Custom instructions

The solution for custom directive type errors is almost exactly the same as for native event listening. For local registers, use either of the first two solutions. For global registers, use solution 3.

Error scenario – Custom event

Now let’s take a look at the custom events and modify click.tsx and app.tsx:

// ClickMe.tsx
import { defineTypeComponent } from '@/utils'

const ClickMePropsDefine = {
  title: {
    type: String.required: true
  },
  onHello: Function
} as const

export default defineTypeComponent({
  name: 'Click Me'.props: ClickMePropsDefine,
  emits: ['hello'],
  setup (props, { emit }) {
    const handleClick = () = > {
      emit('hello')}return () = > (
      <div onClick={ handleClick} >{ props.title }</div>)}})// App.tsx
import ClickMe from '@/components/ClickMe'

export default {
  name: 'App',
  setup () {
    const handleHello = () = > {
      console.log('Hello World')}return () = > (
      <div>
        <ClickMe title="点我" onHello={ handleHello} / >
      </div>)}}Copy the code

Sure enough, the compiler tells you that onHello is not on the component, so adding custom events to BasePropsDefine doesn’t make sense, so solution 3 above can be ruled out, while the other two methods work.

Custom events – Solution 1

Same as native events – Solution one

Custom events – Solution 2

Same as native Events – Solution two

Custom events – Solution 3

Solution one bypassed the type check and intelligent prompt, solution two was too cumbersome, here is a very simple method, add onHello to the props definition, simple and effective to pass the type check, without interfering with the normal execution of the listener event

// ClickMe.tsx
const ClickMePropsDefine = {
  title: {
    type: String.required: true
  },
  onHello: Function
} as const
Copy the code

Again, why don’t you do this when listening for native events? The listener for a native event will emit events such as click from the subcomponent. The listener for a native event will not emit events such as click from the subcomponent. The listener for a native event will not emit events.

Extended thinking

In template, we often use the attrs attribute to let the parent component directly pass the value to the grandson component, the grandson component props type is obviously not defined in the child component, so the parent component directly pass the value to the grandson component is bound to cause the compiler error, here also provides a few ideas, for your reference.

Thinking a

As in solution 1 above, use global or local component registration and kebab-Case naming to bypass type checking.

Idea 2

As in solution 2 above, add the props definition for the sun component to the PropsDefineWrapper.

Thought three

As with native events – Solution 3, extend BasePropsDefine

// @/types/index.ts
export type BasePropsDefine = {
  readonly onClick: FunctionConstructor;
  readonly [key: string] :any;
}
/ / or
export type BasePropsDefine = {
  readonly onClick: FunctionConstructor;
} & Record<string.any>
Copy the code

However, with this definition, the type checking and intelligence hints are gone, and the functionality of TS is greatly reduced.

Thinking summary

Each of these approaches ensures that the compiler does not report errors, but each has its own drawbacks. Idea 1 and idea 3 do not enjoy the blessing of TS, idea 2 code is too tedious, and custom event solution 3 does not work, because it will receive the corresponding property in the child component, not through attR to the grandson component. At present, I haven’t thought of a more perfect idea, so I can update it after I have it.