Kurs Vue.js · Moduł 15: TypeScript i Nuxt

Typowanie Emits

2 min czytania
W tej lekcji5

Tak jak propsy definiują dane wejściowe komponentu, emity definiują zdarzenia wyjściowe. W TypeScript możemy precyzyjnie typować jakie zdarzenia komponent emituje i jakie dane niesie każde zdarzenie.

defineEmits z generycznym typem

1<script setup lang="ts">
2// Składnia generyczna
3const emit = defineEmits<{
4  (e: 'update', value: string): void
5  (e: 'delete', id: number): void
6  (e: 'close'): void
7}>()
8
9// Użycie
10emit('update', 'new value')  // OK
11emit('delete', 42)           // OK
12emit('close')                // OK
13// emit('update', 123)       // BŁĄD! Oczekiwany string, nie number
14// emit('unknown')           // BŁĄD! Nieznane zdarzenie
15</script>

Nowa składnia 3.3+

Od Vue 3.3 dostępna jest krótsza składnia:

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

Obie składnie są równoważne. Nowa jest bardziej zwięzła - używamy nazwy zdarzenia jako klucza, a argumenty jako krotki (tuple).

Typowane zdarzenia w praktyce

Oto przykład panelu sterowania stacją:

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', 'Anomalia kwantowa wykryta!')
22}
23</script>

Komunikacja rodzic-dziecko

Komponent dziecko emituje zdarzenia, rodzic nasłuchuje:

1<!-- ControlPanel.vue (dziecko) -->
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    Skanuj Sektor 7
11  </button>
12</template>
1<!-- App.vue (rodzic) -->
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 zna typy argumentów!
10}
11</script>

Łączenie Props i Emits

Wzorzec v-model z typami:

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>
Kod do tej lekcji: 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 - Typowane Emits</h1>
33
34    <CommandPanel
35      @command="handleCommand"
36      @alert="handleAlert"
37    />
38
39    <div class="log-section">
40      <h2>Dziennik komend</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">Brak komend</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>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Która składnia poprawnie definiuje typowane emity w Vue 3.3+?

  2. 2. Mając const emit = defineEmits<{ alert: [level: string, msg: string] }>(), które wywołanie jest poprawne?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Klikanie w kolejności

    Ułóż kroki implementacji typowanego v-model w komponencie:

  • Układanie w pionie

    Uporządkuj przepływ danych w typowanej komunikacji rodzic-dziecko:

Przydatne artykuły