Learn the method for triggering events with a strongly-typed payload in Vue 3 Composition API and TypeScript

I'm currently exploring Vue 3 Composition API along with TypeScript, particularly focusing on emitting events with a strictly typed payload.

There's an example provided below, but I'm unsure if it's the most effective way to achieve this. Are there any alternative approaches to emit events with a strictly typed payload?


Sample

Utilizing the package: https://www.npmjs.com/package/vue-typed-emit, I managed to make it work by passing a boolean value from a child component to its parent as demonstrated below:

Child Component:

<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import { CompositionAPIEmit } from 'vue-typed-emit'

interface ShowNavValue {
showNavValue: boolean
}
interface ShowNavValueEmit {
emit: CompositionAPIEmit<ShowNavValue>
}

export default defineComponent({
name: 'Child',
emits: ['showNavValue'],

setup(_: boolean, { emit }: ShowNavValueEmit) {
let showNav = ref<boolean>(false)

watch(showNav, (val: boolean) => {
emit('showNavValue', val)
})

return {
showNav
}
}
})
</script>

Parent Component

<template>
<div id="app">
<Child @showNavValue="toggleBlurApp" />
<div :class="{'blur-content': blurApp}"></div>
</div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import Child from './components/Child.vue';

export default defineComponent({
name: 'Parent',
components: {
Child
},

setup() {
let blurApp = ref<boolean>(false);

let toggleBlurApp = (val: boolean) => {
blurApp.value = val;
}

return { 
blurApp, 
toggleBlurApp 
}
});
</script>

<style lang="scss">
.blur-content{
filter: blur(5px); 
transition : filter .2s linear;
}
</style>

Answer №1

Introducing Vue's new <script setup> compiler macro designed for declaring a component's emitted events effortlessly. The argument expected is identical to the component's emits option.

Take a look at this runtime declaration example:

const emit = defineEmits(['change', 'update'])

Below is an example of type-based declaration:

const emit = defineEmits<{
  (event: 'change'): void
  (event: 'update', id: number): void
}>()

emit('change')
emit('update', 1)

Remember, this feature can only be utilized within <script setup>, it gets optimized in the final output, and should not actually be invoked during runtime.

Answer №2

As of June 2023: The usage of defineEmits with the setup script renders this answer outdated, but it's still possible to extend Vue in order to incorporate types on the setup context object. It's important to note that this method is only compatible with versions preceding 3.2.46:

One can avoid installing vue-typed-emit and instead opt for this approach: Begin by defining an interface specifying the structure your events should adhere to, where the event key is 'event' and the type represents the emitted event's type 'args'.

interface Events {
    foo?: string;
    bar: number;
    baz: { a: string, b: number };
}

You can then import and utilize the existing SetupContext interface from Vue, extending it to impose additional constraints on the emit functions parameters.

interface SetupContextExtended<Event extends Record<string, any>> extends SetupContext {
    emit: <Key extends keyof Event>(event: Key, payload: Event[Key]) => void;
}

This interface essentially supersedes the current

emit(event: string, args: any) => void
with an emit function that receives 'event' as a key of the 'Events' interface and its corresponding type as 'args'.

Subsequently, you can define the setup function within the component, substituting SetupContext with SetupContextExtended while supplying the 'Events' interface.

    setup(props, context: SetupContextExtended<Events>) {
        context.emit('foo', 1);                 // TypeError - 1 should be string
        context.emit('update', 'hello');        // TypeError - 'update' does not exist on type Events
        context.emit('foo', undefined);         // Success
        context.emit('baz', { a: '', b: 0 });   // Success
    }

Example of a functional component:

<script lang="ts">
import { defineComponent, SetupContext } from 'vue';

interface Events {
    foo?: string;
    bar: number;
    baz: { a: string, b: number };
}

interface SetupContextExtended<Event extends Record<string, any>> extends SetupContext {
    emit: <Key extends keyof Event>(event: Key, payload: Event[Key]) => void;
}

export default defineComponent({
    name: 'MyComponent',
    setup(props, context: SetupContextExtended<Events>) {
        context.emit('foo', 1);                 // TypeError - 1 should be string
        context.emit('update', 'hello');        // TypeError - 'update' does not exist on type Events
        context.emit('foo', undefined);         // Success
        context.emit('baz', { a: '', b: 0 });   // Success
    }
});
</script>

To make this extended type available across all new and existing components - You have the option to enhance the Vue module itself to include the custom SetupContextExtended interface in your current imports. In this illustration, it's included in shims-vue.d.ts, however, feel free to place it in a separate file if desired.

// shims-vue.d.ts
import * as vue from 'vue';

// Pre-existing content
declare module '*.vue' {
    import type { DefineComponent } from 'vue';
    const component: DefineComponent<{}, {}, any>;
    export default component;
}

declare module 'vue' {
    export interface SetupContextExtended<Event extends Record<string, any>> extends vue.SetupContext {
        emit: <Key extends keyof Event>(event: Key, payload: Event[Key]) => void;
    }
}

Finalized component with enhanced Vue module:

<script lang="ts">
import { defineComponent, SetupContextExtended } from 'vue';

interface Events {
    foo?: string;
    bar: number;
    baz: { a: string, b: number };
}

export default defineComponent({
    name: 'MyComponent',
    setup(props, context: SetupContextExtended<Events>) {
        context.emit('baz', { a: '', b: 0 });   // Success
    }
});
</script>

Personally, I prefer to define and export the Events interface in the parent component and then import it into the child component so that the parent dictates the contract governing the emit events of the child.

Answer №3

My current approach involves using

<script setup lang="ts">
to strongly type and validate the payload of my emits:

<script setup lang="ts">
defineEmits({
  newIndex(index: number) {
    return index >= 0
  },
})

// const items = [{ text: 'some text' }, ...]
</script>

I then proceed to emit events in the following manner:

<template>
  <div
    v-for="(item, index) in items"
    :key="index"
    @click="$emit('newIndex', index)"
  >
    {{ item.text }}
  </div>
</template>

If I only need to declare and type the emit without implementing its behavior, I would use this syntax:

defineEmits<{
  (event: 'newIndex', index: number): void
}>()

Answer №4

If you are utilizing <script setup>, then utilizing defineEmits (as suggested by guangzan) is suitable. You can refer to the official documentation at: https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits

Alternatively, if you prefer using

<script lang="ts">
with a setup function, there are 2 options for specifying strongly typed event payloads.

Option #1

You can check out the informative answer provided by Kiaan Edge-Ford. This option is particularly useful if you wish to enforce consistent Events across multiple components.

Option #2 (more concise)

It is not explicitly stated in the official Vue documentation yet, but emits: actually supports an alternative format. Instead of an array of strings, it can be an object of functions. For example:

export default defineComponent({
  props: ...
  emits: ['set-field', 'update:is-valid']
  ...

You can transform it into something like this:

export default defineComponent({
  props: ...
  emits: {
    // eslint-disable-next-line unused-imports/no-unused-vars, no-useless-computed-key, object-shorthand
    ['set-field'](payload: { partA: number, partB: string, partC: boolean }) { return true; },
    // eslint-disable-next-line unused-imports/no-unused-vars, no-useless-computed-key, object-shorthand
    ['update:is-valid'](payload: boolean) { return true; },
  },
  ...

The eslint comments may or may not be necessary to avoid warnings. The purpose of the function here is validation, so returning true signifies that it is always valid.

With this setup, your environment will alert you if the corresponding emit(...) function does not send the correct payload.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I am attempting to gather user input for an array while also ensuring that duplicate values are being checked

Can someone assist me with the following issue: https://stackblitz.com/edit/duplicates-aas5zs?file=app%2Fapp.component.ts,app%2Fapp.component.html I am having trouble finding duplicate values and displaying them below. Any guidance would be appreciated. I ...

Include a scrollbar within a Bootstrap table grid under a div with a width of 12 columns

How can I add a scrollbar to the grid below? It is using a bootstrap table inside a bootstrap col 12 div. I have attempted to use the following CSS, but it does not apply a scrollbar, it only disrupts the columns. divgrid.horizontal { width: 100%; ...

Exploring TypeScript: Navigating the static methods within a generic class

I am trying to work with an abstract class in TypeScript, which includes an enum and a method: export enum SingularPluralForm { SINGULAR, PLURAL }; export abstract class Dog { // ... } Now, I have created a subclass that extends the abstract cla ...

What is the best way to transfer data received from an observable function to use as an input for another observable function?

After carefully declaring all the variables, I am facing an issue with passing the value obtained from the first observable function (this.acNum) as a parameter to resolve the second observable function within the ngOnInit method. Despite displaying correc ...

TypeScript failing to correctly deduce the interface from the property

Dealing with TypeScript, I constantly encounter the same "challenge" where I have a list of objects and each object has different properties based on its type. For instance: const widgets = [ {type: 'chart', chartType: 'line'}, {typ ...

Populating and dynamically sorting a v-select component in Vuetify

I am working on populating and sorting an array dynamically for use in a v-select component. However, I am encountering the es-lint warning 'unexpected side-effect in computed property' because I am modifying objects within that function call. Is ...

The React Hook Form's useFieldArray feature is causing conflicts with my custom id assignments

My schema includes an id property, but when I implement useFieldArray, it automatically overrides that id. I'm utilizing shadcn-ui Version of react-hook-form: 7.45.0 const { fields, append, remove, update } = useFieldArray<{ id?: string, test?: n ...

Error in Typescript Observable when using .startWith([])

I encountered the TypeScript error below: Error:(34, 20) TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'number | Scheduler'. Type 'undefined[]' is not assignable to type 'Scheduler& ...

What is the best method for transferring updated data from the frontend to the backend without needing to store any unchanged values?

After importing a list from a database using axios and storing it in a variable called tasks, each object may resemble the following structure: tasks: [ { title: 'some text here' }, { completed: false }, ] If there are 2000 or 3000 of ...

Resolving TypeScript error when importing images statically in Next.js

In order to enhance the performance of images in my nextjs app, I am working on image optimization. However, I encountered an issue stating: Cannot find module '/images/homeBg.jpg' or its corresponding type declarations. The image is actually st ...

Is there a way to automatically refresh a page as soon as it is accessed?

My goal is to create a page refresh effect (similar to pressing Command+R on Mac OS) when navigating to a certain page. For instance: Currently, when I navigate from "abc.com/login" to "abc.com/dashboard" after successfully logging in, the transition occ ...

Refresh Ionic 2 Platform

I'm currently working on an Ionic 2 app and whenever I make a change to the .ts code, I find myself having to go through a tedious process. This involves removing the platform, adding the Android platform again, and then running the app in Android or ...

Retrieve vuex state in a distinct axios template js file

I have encountered an issue with my Vue project. I am using Vuex to manage the state and making axios requests. To handle the axios requests, I created a separate file with a predefined header setup like this: import axios from 'axios' import st ...

Utilizing Vuejs and v-model for pre-selecting checkboxes

On my webpage, I have a set of checkboxes that are connected to the v-model with specific values assigned to them. However, I want some of these checkboxes to be pre-checked when the page loads. The challenge is that I can't use the :checked binding ...

The inability to destructure the 'store' property from the 'useReduxContext(...)' because of its null value

I am currently using NextJs 13 along with redux toolkit. Whenever I run the npm run build command, I encounter this error: "Cannot destructure property 'store' of 'useReduxContext(...)' as it is null." I suspect that the issue lies wi ...

Struggling with defining types in NextJs when using dynamic imports in Typescript and NextJs

I have successfully created a component that utilizes next/dynamic to import react-icons only when needed. However, I am struggling to properly define the TypeScript types for this component. Despite this issue, the component itself is functioning as expec ...

refresh the component upon resource modification

I developed a basic to-do app and encountered an issue where deleting an item from my Vuex store triggers automatic updates in the component rendering the to-do list. However, when updating a to-do item from "pending" to "completed," the component fails to ...

Using Vue's mdi icons within a string

What is the best way to incorporate mdi icons into a string and have it rendered as an icon within a method that acts as a component in a cell of an ag-grid table? import { mdiPencil } from "@mdi/js"; data(){ return{ mdiPencil } } methods ...

Learn how to define an array of member names in TypeScript for a specific type

Is there a way to generate an array containing the names of members of a specific type in an expression? For example: export type FileInfo = { id: number title ?: string ext?: string|null } const fileinfo_fields = ["id","ext&qu ...

What could be causing TypeORM to create an additional column in the query

Why does this TypeORM query produce the following result? const result6 = await getConnection() .createQueryBuilder() .select('actor.name') .from(Actor,'actor') .innerJoin('actor.castings',&apos ...