Prop Types and Validation

Declare a prop's type, mark it required, give it a default, and let the dev build warn you when a parent passes the wrong thing.

Syntaxconst { max = 5 } = defineProps<{ max?: number }>()

A prop is a value a parent passes down to a component. Declaring your props does two jobs: it tells Vue which attributes on the tag are data rather than plain HTML attributes, and it describes the shape each value should have.

Two ways to declare

In a TypeScript component you write the types and the compiler produces the runtime declaration for you. In plain JavaScript you pass an object instead. The two object forms below are the same thing, one written out in full:

defineProps({ max: Number })
defineProps({ max: { type: Number } })

The long form is where required: true, default and validator live. A validator receives (value, props) and returns a boolean; the second argument, the full props object, is available from Vue 3.4.

Defaults

A default for a number, string or boolean is a plain value. A default for an object or an array must come from a factory function, because one literal would otherwise be shared by every instance of the component. The factory receives the raw props as its argument.

What trips people up

Validation failures are a development-time console warning. Production builds drop the checks, so a wrong type ships silently.

Boolean casting also depends on type order. With [Boolean, String] a bare disabled attribute becomes true; with [String, Boolean] it becomes an empty string.

From Vue 3.5 you may destructure defineProps and still keep reactivity, which is how the example below sets defaults.

Example

Example · vue
<!-- src/components/ScoreBadge.vue -->
<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  label: string
  score: number
  max?: number
  tags?: string[]
}

const { label, score, max = 5, tags = [] } = defineProps<Props>()

const filled = computed(() => Math.max(0, Math.min(score, max)))
const empty = computed(() => max - filled.value)
</script>

<template>
  <figure>
    <figcaption>{{ label }}</figcaption>
    <p>
      <span v-for="n in filled" :key="`on-${n}`">&#9733;</span>
      <span v-for="n in empty" :key="`off-${n}`">&#9734;</span>
    </p>
    <ul v-if="tags.length > 0">
      <li v-for="tag in tags" :key="tag">{{ tag }}</li>
    </ul>
  </figure>
</template>

<style scoped>
figure {
  margin: 0;
}

p {
  letter-spacing: 0.2em;
}
</style>

When to use it

  • Give a table component a page size that falls back to 25 when the parent omits it.
  • Restrict a button's variant prop to a known set of strings and get a console warning during development when a typo slips in.
  • Pass a configuration object into a chart component without every instance sharing the same default object.
  • Catch a numeric id arriving as a string from a route parameter before it reaches comparison logic.

More examples

The same props as a runtime declaration

A component without TypeScript declares the same information as an options object. The validator returns false for a combination that makes no sense, and Vue logs a warning in the development build. The second validator argument, props, arrived in Vue 3.4. Reach for this form in a plain JavaScript project, and remember it is what the TypeScript compiler generates from your types anyway.

Example · vue
<!-- src/components/StatusPill.vue -->
<script setup>
const props = defineProps({
  // shorthand: type only
  count: Number,

  // longhand for a different prop, with extra options
  label: { type: String, required: true },

  // plain default for a primitive, plus a validator
  variant: {
    type: String,
    default: 'neutral',
    validator(value, props) {
      if (value === 'danger' && props.count === 0) {
        return false
      }
      return ['neutral', 'success', 'danger'].includes(value)
    }
  },

  // factory default for a reference type
  meta: {
    type: Object,
    default: (rawProps) => ({ source: rawProps.label })
  }
})
</script>

<template>
  <span>{{ props.label }} ({{ props.variant }}) {{ props.meta.source }}</span>
</template>

Defaults before and after Vue 3.5

Reactive props destructure is enabled by default from Vue 3.5, which makes plain JavaScript default values the shortest way to declare one. Most existing code and most tutorials still use withDefaults, so you need to read both. The withDefaults form is still supported; it is not deprecated.

Example · vue
<!-- src/components/Pager.vue -->
<script setup lang="ts">
interface Props {
  page?: number
  sizes?: number[]
}

// Vue 3.5+: destructuring keeps reactivity, so defaults go here
const { page = 1, sizes = [10, 25, 50] } = defineProps<Props>()

// Vue 3.4 and earlier needed withDefaults, and a function for
// the array default:
//
// const props = withDefaults(defineProps<Props>(), {
//   page: 1,
//   sizes: () => [10, 25, 50]
// })
</script>

<template>
  <p>Page {{ page }} of sizes {{ sizes.join(', ') }}</p>
</template>

What a failed check looks like

Prop validation is a development aid, not a runtime guarantee. If a wrong value can reach the component from a server response or a URL parameter, check it yourself rather than relying on the warning, because the warning is compiled out of the production build.

Example · ts
// A parent renders:
//   <score-badge label="Latency" :score="'fast'" />
// where score is declared as `score: number`.

// Development build, browser console:
// [Vue warn] Invalid prop: type check failed for prop "score".
//   Expected Number with value NaN, got String with value "fast".

// Production build: no warning, and `score` is the string "fast".
console.log(typeof 'fast')
// Output: string

Discussion

  • Be the first to comment on this lesson.