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

Develop a dynamic product filtering system in VueJS 3 with interdependent parameters

I'm aiming to develop a product filtering system that operates with mutual dependence just like the ones seen on e-commerce platforms. For instance, if I choose option X in the first filter, then the second filter should only show products correspondi ...

Preventing default behaviors in VueJS textarea components

I'm attempting to incorporate a slack-like feature that sends a message only when the exact Enter key is pressed (without holding down the Shift key). Looking at this Vue template <textarea type="text" v-model="message" @keyup.enter.exact="sendMe ...

Ways to transfer selected options from a dropdown menu to a higher-level component

I am currently in the process of configuring a modal component that showcases various data from a specific record to the user. The user is provided with a Bulma dropdown component for each field, enabling them to make changes as needed. To streamline the c ...

Using Rollup alongside @rollup/plugin-babel and Typescript: Anticipated a comma, received a colon instead

I've encountered a problem while working with Rollup 4: [!] RollupError: Expected ',', got ':' (Note that you need plugins to import files that are not JavaScript) src/index.ts (48:19) Although my Babel configuration appears to ...

Modifying the order of Vuetify CSS in webpack build process

While developing a web app using Vue (3.1.3) and Vuetify (1.3.8), everything appeared to be working fine initially. However, when I proceeded with the production build, I noticed that Vue was somehow changing the order of CSS. The issue specifically revol ...

Trouble encountered while retrieving the data structure in order to obtain the outcome [VueJs]

I am facing an issue while trying to save information from an HTTP request into a structure and then store it in local storage. When I attempt to retrieve the stored item and print a specific value from the structure, it does not work! Here is my code: / ...

Utilizing generic union types for type narrowing

I am currently attempting to define two distinct types that exhibit the following structure: type A<T> = { message: string, data: T }; type B<T> = { age: number, properties: T }; type C<T> = A<T> | B<T>; const x = {} as unkn ...

The output of `.reduce` is a singular object rather than an array containing multiple objects

Building on my custom pipe and service, I have developed a system where an array of language abbreviations is passed to the pipe. The pipe then utilizes functions from the site based on these abbreviations. Here is the parameter being passed to the pipe: p ...

Scrolling Vue bootstrap images within a carousel to prevent overflow

I am working with a vue bootstrap carousel that displays images with 100% width. I want to enable vertical scrolling for portrait images so that I can view the bottom of the image. However, I am facing an issue where the scrollbar appears but is inactive ...

Tips for creating a recursive string literal type in Typescript

I need to create a type that represents a series of numbers separated by ':' within a string. For example: '39:4893:30423', '232', '32:39' This is what I attempted: type N = `${number}` | '' type NL = `${ ...

Reasons behind Angular HttpClient sorting JSON fields

Recently, I encountered a small issue with HttpClient when trying to retrieve data from my API: constructor(private http: HttpClient) {} ngOnInit(): void { this.http.get("http://localhost:8080/api/test/test?status=None").subscribe((data)=> ...

Utilize Redux in conjunction with TypeScript to seamlessly incorporate a logout feature

My login page redirects to a private /panel page upon successful login with an accessToken. I am utilizing the Redux store to verify the token in the privateRoute component. Challenges I'm encountering: I aim to enable logout functionality from t ...

Fetching data in Vuejs only upon user interaction

Greetings to all. Thank you for taking the time to read this message. I am currently working on populating a list of items. Within objects1, there is an array containing various data, including a URL for retrieving each item's picture. However, when I ...

Converting a file into a string using Angular and TypeScript (byte by byte)

I am looking to upload a file and send it as type $byte to the endpoint using the POST method My approach involves converting the file to base64, and then from there to byte. I couldn't find a direct way to convert it to byte, so my reasoning may be ...

Iterating through an array with ngFor to display each item based on its index

I'm working with an ngFor loop that iterates through a list of objects known as configs and displays data for each object. In addition to the configs list, I have an array in my TypeScript file that I want to include in the display. This array always ...

What is the best way to add items to arrays with matching titles?

I am currently working on a form that allows for the creation of duplicate sections. After submitting the form, it generates one large object. To better organize the data and make it compatible with my API, I am developing a filter function to group the du ...

Checking the loading status of .gz files in Chrome browser for a VueJs application involves a few simple steps

I currently have a VueJs application set up with webpack. In the webpack.prod.conf.js file, I have configured the productionGzip setting to generate a .gz file for each of the chunks in the dist folder. productionGzip: true, productionGzipExtensions: [&ap ...

Collaborate on Typescript Interfaces within a Firebase development environment

I've been developing a Firebase project using Angular for the frontend, and incorporating the @angular/fire library. Within this project, I have created multiple interfaces that utilize firebase and firestore types. For example: export interface ...

Create a VueJs component and globally register it with a unique custom style

I am looking to integrate Vue-Select into my VueJs project by creating a WebPack configuration that exports separate files for vendors and pages. Vue-Select offers a lot of customization options, and I want to centralize these settings in a single file for ...

Developing a React-based UI library that combines both client-side and server-side components: A step-by-step

I'm working on developing a library that will export both server components and client components. The goal is to have it compatible with the Next.js app router, but I've run into a problem. It seems like when I build the library, the client comp ...