skip to content

In Vue 3, how does the object syntax of defineEmits validate an event payload, and what happens when validation fails?

level: middleimportance: nice to knowfreq 28%

answer

  1. like prop validators
  2. null means no check
  3. return a boolean
  4. development only, warning only

basics

~20 s

With the object syntax, each event maps to null or a function that receives the emit arguments and returns a boolean. A false result logs a development warning, but the event is still delivered, and production builds skip validators.

solid answer

~40 s

Instead of an array, pass an object to `defineEmits`: `{ cancel: null, confirm: (id: string) => id.length > 0 }`. `null` declares the event without checks; a function receives exactly the arguments passed to `emit` and returns `true` or `false`. When it returns a falsy value, Vue logs `Invalid event arguments: event validation failed for event "confirm".` in development, **and still calls the parent's handler**: validation reports bad payloads, it does not block them. Validators run only in development builds, so they cost nothing in production and must never hold business rules. Use them to catch contract mistakes early, alongside TypeScript types that check call sites at compile time.

code

vue · 22 lines
vue
<!-- DeleteButton.vue -->
<script setup lang="ts">
const props = defineProps<{ itemId: string }>()

const emit = defineEmits({
  cancel: null,
  confirm: (id: unknown) => typeof id === 'string' && id.length > 0,
})

function onClick() {
  if (window.confirm('Delete this item?')) {
    // An empty itemId logs a dev warning, but the parent's handler still runs
    emit('confirm', props.itemId)
  } else {
    emit('cancel')
  }
}
</script>

<template>
  <button type="button" @click="onClick">Delete</button>
</template>

go deeper

for a junior

Recall that defineEmits accepts an object where each event maps to null or a function returning true or false.

for a middle

Explain that the validator receives the emit arguments, that failure only warns, and that production builds skip it.

for a senior

Keep business rules out of validators, use them for shared contracts, and pair them with type-based declarations where the compiler can help.

for a principal

Decide where a component library validates event payloads, at compile time or in development runtime, and document the contract per event.

## Two ways to declare events `defineEmits` accepts either form: - an **array of names**, `defineEmits(['confirm', 'cancel'])`, which declares events without any checks - an **object**, where each key is an event name and each value is either `null` or a **validator function** ```vue <script setup lang="ts"> const emit = defineEmits({ cancel: null, confirm: (id: unknown) => typeof id === 'string' && id.length > 0, }) </script> ``` The object form is the same shape as the `emits` option outside `<script setup>`. ## How the validator runs When the component calls `emit('confirm', value)`, a development build does the following, in order: 1. checks that `confirm` is declared (or that an `onConfirm` prop exists) 2. if the declaration is a function, calls it with **the same arguments** passed to `emit` after the name 3. if the function returns a falsy value, logs `Invalid event arguments: event validation failed for event "confirm".` 4. looks up the parent's listener and calls it with the arguments, **regardless of the result** Step 4 is the one candidates get wrong. A failed validator is a **warning**, not a guard. | Aspect | Emit validator | What it is not | |---|---|---| | result `false` | development warning | a cancelled event | | production build | skipped entirely | a runtime safety net | | arguments | exactly what was passed to `emit` | a normalised payload | | return value | only its truthiness is checked | a transformed value | ## When validators are worth it Validators shine on components used by many teams, where the payload shape is part of a contract: - an event carrying an **identifier** that must not be empty - an event carrying an **object** whose required fields must be present - an event whose payload changed shape during a refactor, where the warning catches stale emit calls Because validators receive the raw arguments, they can also check the **count** of arguments, which helps when an event was changed from positional arguments to a single object. ## Validators and TypeScript With TypeScript, a type-based `defineEmits` declaration checks every `emit` call at compile time, which usually makes runtime validators redundant for internal components. The two are complementary rather than exclusive: - types catch mistakes in code the compiler sees, at build time - validators catch values that only exist at runtime, such as data from an API, in development The object syntax also lets you annotate validator parameters, which types the corresponding `emit` call. ## Designing payloads that validate well Validators are easiest to write and read when the payload is simple and stable: - prefer **one object payload**, such as `{ id, reason }`, over several positional arguments, so the validator checks named fields - keep the payload **serialisable data**, not DOM events or component instances, so it can be checked with plain type tests - validate the **shape**, not the business meaning: "id is a non-empty string" belongs in a validator, "this user may delete this item" does not A small helper that returns `true` or logs a precise message and returns `false` makes the development warning actionable, because Vue's own message only names the event, not the field that failed. | Check | Belongs in the validator? | |---|---| | payload is an object with an `id` string | yes | | `reason` is one of the allowed values | yes | | the item still exists on the server | no, that is an asynchronous business check | | the user has permission to delete | no, it must also hold in production | ## Pitfalls - **Business rules in validators.** A validator that returns `false` for an unauthorised deletion does not stop the deletion, and in production it does not even run. Keep real checks in the handler or the service call. - **Side effects.** A validator runs on every emit in development only, so logging, counting or mutating state there makes development and production behave differently. - **Forgetting to return.** A validator that only logs and returns `undefined` is falsy, so it warns on every emit.

  • If a Vue emit validator returns false, can the component rely on the parent never seeing that payload?
    No. Vue only logs a development warning and then calls the listener with the same arguments. In production the validator does not run at all. Guard the payload in the component's own code before calling `emit` if it must not go out.
  • When is a runtime emit validator still useful if the project already uses a type-based defineEmits?
    When the payload comes from runtime data the compiler cannot see, such as an API response, or when the component is consumed from plain JavaScript. The validator then flags bad payloads during development that types could not.

saying these in an interview costs you the question

  • A validator returning false cancels the event so the parent never receives it.
  • Emit validators run in production and protect against bad data.
  • The validator can return a corrected payload that replaces the original.
  • Declaring an event with null makes emitting it an error.
  • Validators receive a DOM event object rather than the emit arguments.