Typing Emits and Slots

+15 Mana ✨

Type-safe events and slots ensure proper communication between components.

Type-Based Emits

Call Signature Syntax

vue
<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
  (e: 'delete'): void  // No payload
}>()

// Usage
emit('change', 123)       // ✅
emit('update', 'hello')   // ✅
emit('delete')            // ✅
emit('change', 'wrong')   // ❌ Type error: string not assignable to number
emit('unknown')           // ❌ Type error: unknown event
</script>

Named Tuple Syntax (Vue 3.3+)

vue
<script setup lang="ts">
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
  submit: [data: FormData, silent?: boolean]
  delete: []  // No payload
}>()

// More concise and readable
emit('submit', formData, true)
</script>

Complex Event Payloads

vue
<script setup lang="ts">
interface User {
  id: number
  name: string
}

interface FilterOptions {
  search: string
  status: 'all' | 'active' | 'inactive'
  page: number
}

const emit = defineEmits<{
  select: [user: User]
  filter: [options: FilterOptions]
  sort: [field: keyof User, direction: 'asc' | 'desc']
}>()
</script>

Runtime Emits with Validation

vue
<script setup lang="ts">
const emit = defineEmits({
  // Validate submit payload
  submit: (payload: { email: string; password: string }) => {
    if (!payload.email.includes('@')) {
      console.warn('Invalid email')
      return false
    }
    return true
  },
  
  // No validation, just type
  cancel: null
})
</script>

Typing Slots

defineSlots (Vue 3.3+)

vue
<script setup lang="ts">
const slots = defineSlots<{
  // Default slot with no props
  default(): any
  
  // Named slot with props
  header(props: { title: string }): any
  
  // Scoped slot
  item(props: { item: Item; index: number }): any
  
  // Optional slot
  footer?(): any
}>()

// Check if slot is provided
if (slots.footer) {
  // TypeScript knows footer exists here
}
</script>

<template>
  <div>
    <header>
      <slot name="header" title="Page Title" />
    </header>
    
    <main>
      <slot />  <!-- default -->
    </main>
    
    <ul>
      <li v-for="(item, index) in items" :key="item.id">
        <slot name="item" :item="item" :index="index" />
      </li>
    </ul>
    
    <footer v-if="slots.footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

Checking Slot Existence

vue
<script setup lang="ts">
import { useSlots } from 'vue'

const slots = useSlots()

// Check if slots are provided
const hasHeader = computed(() => !!slots.header)
const hasFooter = computed(() => !!slots.footer)
</script>

v-model Typing

defineModel (Vue 3.4+)

vue
<script setup lang="ts">
// Basic v-model
const modelValue = defineModel<string>()

// Required v-model
const modelValue = defineModel<string>({ required: true })

// With default
const modelValue = defineModel<string>({ default: '' })

// Named v-model
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')

// With transform
const count = defineModel<number>({
  get(value) {
    return value ?? 0
  },
  set(value) {
    return Math.max(0, value)  // Ensure non-negative
  }
})
</script>

Manual v-model Typing

vue
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

// Two-way binding helper
const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>

Complete Component Example

vue
<script setup lang="ts">
import type { Component } from 'vue'

// Props
interface Props {
  title: string
  items: Item[]
  loading?: boolean
  icon?: Component
}

const props = withDefaults(defineProps<Props>(), {
  loading: false
})

// Emits
const emit = defineEmits<{
  select: [item: Item]
  delete: [id: number]
  'update:items': [items: Item[]]
}>()

// Slots
const slots = defineSlots<{
  default(): any
  header(props: { count: number }): any
  item(props: { item: Item; index: number }): any
  empty(): any
}>()

// v-model for items
const items = defineModel<Item[]>('items', { default: () => [] })
</script>
✓ Completed