Compiler Macros: defineProps, defineEmits, and More

+15 Mana ✨

<script setup> provides special compiler macros that don't need to be imported. They're transformed at compile time.

defineProps

Declare the props your component accepts:

vue
<script setup lang="ts">
// Runtime declaration
const props = defineProps({
  title: String,
  count: {
    type: Number,
    required: true
  },
  disabled: {
    type: Boolean,
    default: false
  }
})

// Type-based declaration (recommended with TypeScript)
const props = defineProps<{
  title?: string
  count: number
  disabled?: boolean
}>()

// Access props
console.log(props.count)
</script>

withDefaults for Type-Based Props

vue
<script setup lang="ts">
type Props = {
  msg?: string
  labels?: string[]
  count?: number
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'Hello',
  labels: () => ['one', 'two'],  // Factory for non-primitives
  count: 0
})
</script>

defineEmits

Declare events your component can emit:

vue
<script setup lang="ts">
// Runtime declaration
const emit = defineEmits(['update', 'delete'])

// Type-based declaration
const emit = defineEmits<{
  (e: 'update', id: number, value: string): void
  (e: 'delete', id: number): void
}>()

// Vue 3.3+ shorthand
const emit = defineEmits<{
  update: [id: number, value: string]
  delete: [id: number]
}>()

// Emit events
emit('update', 1, 'new value')
emit('delete', 1)
</script>

defineModel (Vue 3.4+)

Simplify two-way binding for v-model:

vue
<!-- Child component -->
<script setup>
// Replaces props + emit pattern for v-model
const modelValue = defineModel()

// With options
const count = defineModel('count', { 
  type: Number, 
  default: 0 
})

// TypeScript
const modelValue = defineModel<string>()
const count = defineModel<number>('count', { required: true })
</script>

<template>
  <input :value="modelValue" @input="modelValue = $event.target.value" />
</template>

<!-- Parent -->
<template>
  <MyInput v-model="text" v-model:count="counter" />
</template>

defineExpose

Expose properties to parent via template refs:

vue
<!-- Child -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const privateData = ref('secret')

function reset() {
  count.value = 0
}

// Only these are accessible via template ref
defineExpose({
  count,
  reset
})
</script>

<!-- Parent -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = ref<InstanceType<typeof Child> | null>(null)

onMounted(() => {
  console.log(childRef.value?.count)  // Works
  childRef.value?.reset()             // Works
  // childRef.value?.privateData      // undefined
})
</script>

<template>
  <Child ref="childRef" />
</template>

defineOptions (Vue 3.3+)

Set component options without a separate <script> block:

vue
<script setup>
defineOptions({
  name: 'CustomName',
  inheritAttrs: false,
  customOptions: {
    /* ... */
  }
})
</script>

defineSlots (Vue 3.3+)

Provide type hints for slots:

vue
<script setup lang="ts">
const slots = defineSlots<{
  default(props: { message: string }): any
  header(props: { title: string }): any
  footer(): any
}>()
</script>

useSlots and useAttrs

Access slots and fallthrough attributes:

vue
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

// Check if slot exists
if (slots.header) {
  console.log('Header slot provided')
}

// Access fallthrough attrs
console.log(attrs.class, attrs.style)
</script>
✓ Completed