Vue.js course Β· Module 15: TypeScript & Nuxt

Typing Emits

2 min read
In this lesson5

Just as props define the input data of a component, emits define its output events. With TypeScript, we can precisely type what events a component emits and what data each event carries.

defineEmits with Generic Type

1<script setup lang="ts">
2// Generic syntax
3const emit = defineEmits<{
4  (e: 'update', value: string): void
5  (e: 'delete', id: number): void
6  (e: 'close'): void
7}>()
8
9// Usage
10emit('update', 'new value')  // OK
11emit('delete', 42)           // OK
12emit('close')                // OK
13// emit('update', 123)       // ERROR! Expected string, not number
14// emit('unknown')           // ERROR! Unknown event
15</script>

New Syntax 3.3+

Since Vue 3.3, a shorter syntax is available:

1<script setup lang="ts">
2const emit = defineEmits<{
3  update: [value: string]
4  delete: [id: number]
5  close: []
6}>()
7</script>

Both syntaxes are equivalent. The new one is more concise - we use the event name as the key and arguments as a tuple.

Typed Events in Practice

Here's an example of a station control panel:

1<script setup lang="ts">
2interface ModuleEvent {
3  moduleId: string
4  timestamp: number
5}
6
7const emit = defineEmits<{
8  activate: [event: ModuleEvent]
9  deactivate: [event: ModuleEvent]
10  alert: [level: 'low' | 'medium' | 'high', message: string]
11}>()
12
13function handleActivation(moduleId: string) {
14  emit('activate', {
15    moduleId,
16    timestamp: Date.now()
17  })
18}
19
20function triggerAlert() {
21  emit('alert', 'high', 'Quantum anomaly detected!')
22}
23</script>

Parent-Child Communication

The child component emits events, the parent listens:

1<!-- ControlPanel.vue (child) -->
2<script setup lang="ts">
3const emit = defineEmits<{
4  command: [action: string, target: string]
5}>()
6</script>
7
8<template>
9  <button @click="emit('command', 'scan', 'sector-7')">
10    Scan Sector 7
11  </button>
12</template>
1<!-- App.vue (parent) -->
2<template>
3  <ControlPanel @command="handleCommand" />
4</template>
5
6<script setup lang="ts">
7function handleCommand(action: string, target: string) {
8  console.log(action, target)
9  // TypeScript knows the argument types!
10}
11</script>

Combining Props and Emits

The v-model pattern with types:

1<script setup lang="ts">
2const props = defineProps<{
3  modelValue: string
4}>()
5
6const emit = defineEmits<{
7  'update:modelValue': [value: string]
8}>()
9
10function onInput(event: Event) {
11  const target = event.target as HTMLInputElement
12  emit('update:modelValue', target.value)
13}
14</script>
15
16<template>
17  <input :value="modelValue" @input="onInput" />
18</template>
Code for this lesson: App.vue
1<script setup lang="ts">
2import { ref } from 'vue'
3import CommandPanel from './CommandPanel.vue'
4
5interface CommandLog {
6  action: string
7  target: string
8  timestamp: string
9}
10
11const logs = ref<CommandLog[]>([])
12
13function handleCommand(action: string, target: string) {
14  logs.value.unshift({
15    action,
16    target,
17    timestamp: new Date().toLocaleTimeString()
18  })
19}
20
21function handleAlert(level: 'low' | 'medium' | 'high', message: string) {
22  logs.value.unshift({
23    action: 'ALERT [' + level + ']',
24    target: message,
25    timestamp: new Date().toLocaleTimeString()
26  })
27}
28</script>
29
30<template>
31  <div class="nova-lab">
32    <h1>NOVA LAB - Typed Emits</h1>
33
34    <CommandPanel
35      @command="handleCommand"
36      @alert="handleAlert"
37    />
38
39    <div class="log-section">
40      <h2>Command log</h2>
41      <div v-for="(log, i) in logs" :key="i" class="log-entry">
42        <span class="time">{{ log.timestamp }}</span>
43        <span class="action">{{ log.action }}</span>
44        <span class="target">{{ log.target }}</span>
45      </div>
46      <p v-if="logs.length === 0" class="empty">No commands</p>
47    </div>
48  </div>
49</template>
50
51<style scoped>
52.nova-lab {
53  background: #0a0e27;
54  color: #00ff88;
55  min-height: 100vh;
56  padding: 20px;
57  font-family: monospace;
58}
59h1 { color: #00ff88; margin-bottom: 15px; }
60h2 { color: #00b4d8; margin-top: 20px; }
61.log-section { margin-top: 15px; }
62.log-entry {
63  display: flex;
64  gap: 12px;
65  padding: 8px 12px;
66  margin: 4px 0;
67  background: rgba(0,180,216,0.06);
68  border-radius: 6px;
69  font-size: 13px;
70}
71.time { color: #666; }
72.action { color: #00b4d8; font-weight: bold; }
73.target { color: #00ff88; }
74.empty { color: #555; font-style: italic; }
75</style>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which syntax correctly defines typed emits in Vue 3.3+?

  2. 2. Given const emit = defineEmits<{ alert: [level: string, msg: string] }>(), which call is correct?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Click in order

    Arrange the steps for implementing a typed v-model in a component:

  • Vertical ordering

    Arrange the data flow in typed parent-child communication:

Useful articles