How to emit events with a strictly typed payload? | Vue 3 Composition API + TypeScript How to emit events with a strictly typed payload? | Vue 3 Composition API + TypeScript vue.js vue.js

How to emit events with a strictly typed payload? | Vue 3 Composition API + TypeScript


Vue <script setup> compiler macro for declaring a component's emittedevents. The expected argument is the same as the component emits option.

Example runtime declaration:

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

Example type-based decalration:

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

This is only usable inside <script setup>, is compiled away in theoutput and should not be actually called at runtime.


I'm using <script setup lang="ts"> and I'm strongly typing AND validating my emit's payload like this:

<script setup lang="ts">defineEmits({  newIndex(index: number) {    return index >= 0  },})// const items = [{ text: 'some text' }, ...]</script>

Then emitting events like this:

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

If I only wanted to declare and type the emit above, I'd do something like this:

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


Installing vue-typed-emit is unnessesary and can be replaced by using this method: Firstly you can define the interface in which you want your events to conform to where the event key is 'event' and the type is the event's emitted type 'args'.

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

You can then import and make use of the existing SetupContext interface from vue and define an extension of this with added restrictions 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 replaces the existing emit(event: string, args: any) => void with an emit function that accepts 'event' as a key of the 'Events' interface and its corresponding type as 'args'.

We can now define our setup function in the component, replacing SetupContext with SetupContextExtended and passing in 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    }

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

Now to make this extended type available in all existing and future components - You can then augment the vue module itself to include this custom SetupContextExtended interface in your existing imports. For this example its added into shims-vue.d.ts but you should be able to add it to a dedicated file if that is desired.

// shims-vue.d.tsimport * as vue from 'vue';// Existing stuffdeclare 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;    }}

Final component with augmented 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>

Using this I personally define and export the Events interface in the parent component and import it into the child so the parent defines the contract governing the child's emit events