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
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.