Suggest an editImprove this articleRefine the answer for “defineProps, defineEmits, and defineExpose in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)`defineProps`, `defineEmits`, and `defineExpose` are Vue 3 compiler macros available in `<script setup>` without imports. `defineProps` declares accepted props with TypeScript types, `defineEmits` declares events the component can fire, and `defineExpose` controls what parents see via template ref. ```ts const props = defineProps<{ count: number }>() const emit = defineEmits<{ (e: 'update:count', value: number): void }>() defineExpose({ reset }) ``` **Key:** Nothing in `<script setup>` is public by default. `defineExpose` is the only way to open the component to a parent ref.Shown above the full answer for quick recall.Answer (EN)Image`defineProps`, `defineEmits`, and `defineExpose` are **compiler macros** in Vue 3's `<script setup>` that declare what a component accepts, signals out, and lets parents access, all without manual imports. ## Theory ### TL;DR - Think of a component as an apartment: `defineProps` is the door lock (what comes in), `defineEmits` is the doorbell (what signals out), `defineExpose` opens a specific window (what an outsider can reach) - Main difference from Options API: these macros give TypeScript inference automatically, no manual `props: {}` object needed - `defineProps` when a parent passes data, `defineEmits` for any event or v-model, `defineExpose` only when a parent needs to call a method imperatively - In `<script setup>`, nothing is public by default, so `defineExpose` is the exception, not the rule ### Quick example ```vue <script setup lang="ts"> // Typed props - no import needed const props = defineProps<{ title: string; count: number }>() // Typed events const emit = defineEmits<{ (e: 'update:count', value: number): void }>() const localCount = ref(props.count) function increment() { localCount.value++ emit('update:count', localCount.value) // signals parent } // Expose only what the parent truly needs defineExpose({ increment }) </script> ``` The parent calls `childRef.value.increment()` via template ref. Everything else stays private. ### Key difference from Options API Options API declares `props`, `emits`, and `expose` as plain objects inside `defineComponent`. Compiler macros do the same thing at build time, but Vue's TypeScript plugin picks up the generic types and gives full inference in the template and in parent components. No runtime overhead difference between the two approaches. ### When to use - Parent passes data → `defineProps` (always first in `<script setup>`) - Child notifies parent or supports v-model → `defineEmits` - Parent needs to call a method on the child directly → `defineExpose` (rare; prefer events for data flow) - Default values on optional typed props → `withDefaults(defineProps<...>(), { count: 0 })` ### How the compiler handles this Vite's SFC plugin transforms macros during build. `defineProps()` becomes a validated `__props` binding; `defineEmits()` registers event validators in the component options; `defineExpose()` sets an `exposeProxy` on the instance so only listed properties are visible when a parent reads `ref.value`. Nothing ships to the browser as a function call named `defineProps`. It compiles away entirely. ### Common mistakes **1. Array syntax without types** ```ts // Wrong - no TypeScript inference, complex shapes become `any` defineProps(['title', 'count']) // Right defineProps<{ title: string; count: number }>() ``` **2. Array or object default as a literal, not a factory** ```ts // Wrong - all instances share one array reference withDefaults(defineProps<{ items?: string[] }>(), { items: [] }) // Right withDefaults(defineProps<{ items?: string[] }>(), { items: () => [] }) ``` Every instance gets its own array. The same bug existed in Vue 2's `data` option, and it still catches people off guard. **3. Mutating props directly** ```ts // Wrong - Vue warns in dev mode, reactivity skips the update props.config.theme = 'dark' // Right - emit the change emit('update:config', { ...props.config, theme: 'dark' }) ``` Props are readonly. Mutating nested objects bypasses the reactivity system and triggers a Vue warning in development. **4. Exposing reactive state instead of methods** ```ts // Risky - parent can read AND write internal state defineExpose({ count, data }) // Better - parent can only trigger actions defineExpose({ increment, reset }) ``` Once you expose a ref, the parent can mutate it. That breaks encapsulation and makes bugs much harder to trace. **5. Forgetting `withDefaults` on optional typed props** ```ts // Wrong - optional prop is `undefined` at runtime if not passed defineProps<{ count?: number }>() // props.count → undefined, not 0 // Right withDefaults(defineProps<{ count?: number }>(), { count: 0 }) ``` ### Real-world usage - **Element Plus**: `defineEmits` for `ElTable` row selection events (`update:selection`) - **Nuxt Content**: `defineProps` for document props in Markdown-backed components - **Modal components**: `defineExpose({ open, close })` so a parent can trigger the modal imperatively - **Vitest tests**: mock emitted events with `vi.fn()` on the mounted component wrapper ### Follow-up questions **Q:** What is the TypeScript advantage of `defineProps<{ foo: string }>()`? **A:** The generic form gives exact types to `props.foo` in the template and in the setup function. Runtime-only declaration via an array of strings leaves everything as `any`. **Q:** How does `defineEmits` validate events? **A:** At compile time, the TypeScript plugin checks that `emit('eventName', payload)` matches the declared signature. At runtime, Vue checks event names against the declared list and warns if something unexpected is emitted. **Q:** When does `defineExpose` actually matter? **A:** Only when a parent holds a template ref and needs to call a method or read a value imperatively. In `<script setup>`, the component instance is sealed by default, so without `defineExpose` the parent sees nothing on `ref.value`. **Q:** What does `defineModel` (Vue 3.4+) replace? **A:** It auto-generates `defineProps({ modelValue })` plus `defineEmits(['update:modelValue'])`. Instead of writing both macros for a v-model prop, you write `const model = defineModel<string>()` and bind `v-model="model"` in the template. ## Examples ### Basic: Button with a counter and v-model ```vue <template> <button @click="increment">{{ props.count }}</button> </template> <script setup lang="ts"> const props = defineProps<{ count: number }>() const emit = defineEmits<{ (e: 'update:count', value: number): void }>() function increment() { emit('update:count', props.count + 1) } </script> ``` The parent uses `<CountButton v-model:count="total" />`. The child never owns the count, it only signals that it should change. ### Intermediate: Content editor with multiple events ```vue <template> <div class="editor"> <textarea v-model="content" /> <button @click="publish">Publish</button> </div> </template> <script setup lang="ts"> interface Post { id: string; content: string; published: boolean } const props = withDefaults( defineProps<{ post: Post }>(), { post: () => ({ id: '', content: '', published: false }) } ) const emit = defineEmits<{ (e: 'update:post', post: Post): void (e: 'publish', id: string): void }>() const content = ref(props.post.content) watch(content, (val) => emit('update:post', { ...props.post, content: val })) function publish() { emit('publish', props.post.id) } </script> ``` The component owns the local textarea state but pushes every change up to the parent. This pattern appears in Nuxt Content-style page editors. ### Advanced: Modal with `defineExpose` ```vue <!-- Modal.vue --> <template> <div v-if="visible" class="modal"> <slot /> <button @click="close">Close</button> </div> </template> <script setup lang="ts"> const visible = ref(false) function open() { visible.value = true } function close() { visible.value = false } // Expose only actions, not the raw `visible` ref defineExpose({ open, close }) </script> ``` ```vue <!-- Parent.vue --> <script setup lang="ts"> import Modal from './Modal.vue' const modal = ref<InstanceType<typeof Modal> | null>(null) function showModal() { modal.value?.open() // type-safe because of defineExpose } </script> <template> <Modal ref="modal"> <p>Content here</p> </Modal> <button @click="showModal">Open</button> </template> ``` The parent can call `open` and `close` but cannot read or write `visible` directly. That is the point of exposing methods instead of state.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.