9

I am trying to learn Vue 3 Composition API with TypeScript, specifically how to emit events with a strictly typed payload.

I have an example below but i'm not sure if it is the correct way. So my question is if there are any other methods to emit events with a strictly typed payload?


Example

I used this package: https://www.npmjs.com/package/vue-typed-emit and got it to work with the example below where I am passing a boolean from a child component to the parent:

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>
AlbinR
  • 396
  • 3
  • 8

4 Answers4

3

Vue <script setup> compiler macro for declaring a component's emitted events. 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 the output and should not be actually called at runtime.

yun_jay
  • 1,050
  • 6
  • 20
guangzan
  • 41
  • 5
2

June 2023 Edit: Using defineEmits with setup script makes this answer obsolete but you can also extend vue to get types on the setup context object. As pointed out however, this will only work with versions prior to 3.2.46:

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.ts
import * as vue from 'vue';

// Existing stuff
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;
    }
}

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

  • 1
    It seems that Vue release 3.2.46 breaks this method (because SetupContext was changed to a type rather than an interface) – Robert Feb 11 '23 at 22:01
  • It works perfect with `vue3` I was overriding but getting all their events from `import { Events } from 'vue';`. it works in this way: `type InputEvents = (event: Key, payload: Events[Key]) => void;` and `interface InputEventsCustom extends InputEvents { (event: 'update:modelValue', value: any): void }` Using it here: `const emit = defineEmits();` – Albert Tjornejoj Aug 21 '23 at 17:55
1

If you're using <script setup>, then using defineEmits (as proposed by the answer by guangzan) is fine. The link to official documentation here: https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits

If, however, you like to use <script lang="ts"> and a setup function, then you basically have 2 options for specifying strongly typed event payloads.

Option #1

See the excellent answer here by Kiaan Edge-Ford. This option may be appropriate especially if you want to re-use and enforce the same Events on multiple components.

Option #2 (less code)

It's not clear at this time from official Vue documentation, but emits: in fact already takes an additional form. Instead of an array of strings, it can be an object of functions. So you can translate this:

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

to 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; },
  },
  ...

You may or may not need the eslint comments to prevent warnings. The function here is intended to be a validation function, so the return true here means: always valid.

Now your environment should warn you when the associated emit(...) function is not sending the proper payload.

Robert
  • 1,220
  • 16
  • 19
  • Do I need to use any other extension then Volor to get the type info of emmited function argument in parent component, I use definedEmit with type declaration but in parent component the listeners function argument type is always any regardless of what I define in child component – Sohail Faruqui Mar 15 '23 at 05:46
0

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
}>()
zenw0lf
  • 1,232
  • 1
  • 13
  • 22