skip to content

Props Declaration & Validation

defineProps declares a component's inputs with runtime options or TypeScript types, defaults and validators. Interviewers probe Boolean casting, object defaults, and mutating a prop.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Vue 3 `<script setup>` component, how do you declare props with defineProps, and how do runtime and type-based declarations differ?

level: juniorimportance: must knowfreq 76%

answer

  1. compiler macro, no import needed
  2. array, object, or type argument
  3. type, required, default, validator
  4. types compiled into runtime options
  5. one argument form per call

basics

~20 s

defineProps is a script setup compiler macro that declares props with either a runtime argument (a names array, or options with type, required, default and validator) or a TypeScript type argument compiled into runtime options; one call cannot take both.

solid answer

~40 s

`defineProps` is only available inside `<script setup>` and needs no import: the SFC compiler replaces it. The **runtime form** takes an array of names (`defineProps(['userId'])`) or an object whose values are a constructor (`{ userId: Number }`) or a full options object with `type`, `required`, `default` and `validator`. The **type-based form**, `defineProps<{ userId: number; label?: string }>()`, lets the compiler infer the runtime options: in development builds a non-optional key becomes `required: true`, an optional one `required: false`. Either way it returns a reactive props object you read as `props.userId` in script and as `userId` in the template. You pick one form per call; passing both a type argument and a runtime argument is a compile error.

code

vue · 10 lines
vue
<script setup lang="ts">
const props = defineProps<{
  userId: number
  label?: string
}>()
</script>

<template>
  <h2>{{ label ?? 'Profile' }} #{{ props.userId }}</h2>
</template>

go deeper

for a junior

Recall the three runtime shapes and the type-based one, and say that runtime props are optional unless required is true.

for a middle

Explain that defineProps is a compiler macro that emits the props option, and how optional keys in the type map to required: false.

for a senior

Show when you pick the runtime object over the type form, such as needing a validator, and why validation warnings are not a production guard.

for a principal

Argue a library-wide convention for prop declarations: type-based for inference, runtime validators for enum-like inputs, and the cost of each for plain-JavaScript consumers.

## What defineProps is In a Vue 3 single-file component that uses `<script setup>`, a component's inputs are declared with `defineProps()`. It is a **compiler macro**, not a runtime function: the SFC compiler recognises the call, removes it, and emits the equivalent `props` option on the compiled component. That is why it needs no `import` (importing it only earns a compiler warning that it is a macro and no longer needs to be imported) and why it may only appear inside `<script setup>`. The call returns the component's **props object**. It is reactive, so reading `props.userId` inside a `computed` or a template creates a dependency, and the template can reference each declared prop directly by name. ## The runtime declaration forms The runtime argument accepts the same value as the Options API `props` option: | Form | Example | What you get | |---|---|---| | Array of names | `defineProps(['userId', 'label'])` | names only, no type check, all optional | | Constructor shorthand | `defineProps({ userId: Number })` | a type check, still optional | | Full options object | `defineProps({ userId: { type: Number, required: true } })` | type, required, default, validator | The full options object carries four fields: - **`type`** — a native constructor (`String`, `Number`, `Boolean`, `Array`, `Object`, `Date`, `Function`, `Symbol`, `Error`) or a custom class checked with `instanceof`; an array such as `[String, Number]` means any of them. - **`required`** — `false` unless set; runtime-declared props are **optional by default**. - **`default`** — used when the resolved value is `undefined`; object and array defaults must be factory functions. - **`validator(value, props)`** — a custom predicate; the second argument (the resolved props) arrived in 3.4. Failed checks produce console warnings in development builds only; they never stop the render. ```vue <script setup> const props = defineProps({ userId: { type: Number, required: true }, label: { type: String, default: 'Profile' }, size: { type: String, validator: (v) => ['sm', 'md', 'lg'].includes(v) } }) </script> ``` ## The type-based declaration form With `<script setup lang="ts">` you can pass a type argument instead: ```ts const props = defineProps<{ userId: number label?: string }>() ``` The compiler reads the type literal (or an interface, including imported ones since 3.3) and generates runtime options from it. In development builds a required key becomes `{ type: Number, required: true }` and an optional key gets `required: false`; production builds emit a slimmer declaration that keeps only what runtime behaviour needs, such as `Boolean` types for casting. The conversion is AST-based, so types that need real type analysis, such as a conditional type for the whole props object, are not supported. Defaults for this form come from `withDefaults` or, since 3.5, from destructure defaults; both belong to the TypeScript and destructuring topics. ## One form per call You cannot mix the two forms in a single call. `defineProps<{ id: number }>({ id: Number })` fails at compile time with the error that `defineProps()` cannot accept both type and non-type arguments at the same time. Choose by need: 1. **Type-based** when the project uses TypeScript and editor inference matters most. 2. **Runtime object** when you need a `validator`, or the file is plain JavaScript. 3. **Array of names** only for prototypes: it documents nothing and checks nothing. ## Naming and usage details - Declare names in **camelCase** (`userId`); parents may pass them as `user-id` in templates and Vue normalises the casing. - Anything the parent passes that is **not** declared as a prop is not in the props object; it falls through as an attribute instead. - `props` is a single object: keep it whole or read fields from it, because pulling fields out with plain destructuring has its own reactivity rules. - Props are read-only inside the child; changing them is the parent's job. ## Common mistakes - Importing `defineProps` from `vue` and calling it in a regular `<script>` block, where no compiler rewrite happens. - Assuming a prop with only `type: Number` is required. - Treating a failed type check as a runtime guard: it is a development-time warning, not validation of untrusted input. ## How the returned object behaves Whatever form you choose, the runtime result is the same kind of object: - It is **shallowly reactive**: reading `props.label` inside a `computed`, a watcher or the template registers a dependency, so the child updates when the parent passes a new value. - The parent refreshes it on every update; the child never assigns to it. - Declared names are **not duplicated into `$attrs`**, so they do not also land on the root element as HTML attributes. - In the template you write `label`, not `props.label`; the compiler resolves the name to the props object for you. This is why an interviewer often follows up with "what does `defineProps` return?": the answer is a live, read-only view of the parent's current inputs.

  • Can a Vue 3 component use the runtime object form and still get TypeScript inference for props?
    Yes. With `defineProps({ userId: { type: Number, required: true } })` in `<script setup lang="ts">`, Vue infers `userId: number` from the constructor and `required`. For complex shapes you annotate `type: Object as PropType<User>`. The type-based form is simply terser; the runtime form remains useful when you also want a `validator`.
  • Why does a Vue 3 prop declared as `userId` still receive the value when the parent writes `user-id="7"`?
    Vue normalises prop keys to camelCase when it resolves props, so `user-id` and `userId` map to the same declared prop. The convention is camelCase in the declaration and in script, kebab-case in in-DOM templates, where HTML attribute names are case-insensitive. Note `user-id="7"` passes the string `'7'`; use `:user-id="7"` for a number.

saying these in an interview costs you the question

  • Imports defineProps from vue and calls it in a plain script block
  • Believes every declared runtime prop is required by default
  • Thinks the type argument is erased and produces no runtime props
  • Tries to combine a type argument and a runtime object in one call
  • Treats prop type checks as input validation that runs in production
open as a page

A Vue 3 `<UserCard>` edits its `user` object prop in place for inline renaming; what does Vue catch, what slips through, and how do you fix it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Reassigning a prop is blocked with a warning in development builds, but mutating a nested field such as props.user.name is not caught and silently changes the parent's object. Fix it with a copied local draft, a computed, or an emitted event.

open as a page

In Vue 3, how does Boolean casting resolve a prop typed Boolean when the parent omits it, writes it bare, or passes a string?

level: middleimportance: should knowfreq 50%

basics

~20 s

A prop whose type includes Boolean is cast: absent becomes false instead of undefined unless a default is declared, and a bare attribute or a value equal to the kebab-case prop name becomes true. Any other string, including "false", is passed as that string.

open as a page

In Vue 3, why must an Object or Array prop's default be a factory function, and when exactly is that default applied?

level: middleimportance: should knowfreq 55%

basics

~20 s

A prop's options are defined once per component, so a literal object or array default would be one shared reference that every instance could mutate. A factory returns a fresh value per instance and runs only when the resolved value is undefined.

open as a page

In Vue 3, what actually happens when a prop fails its type, required or validator check, and what can a validator see?

level: middleimportance: should knowfreq 42%

basics

~20 s

Vue only logs a console warning, and only in development builds; the value is still passed and the component renders. Production builds skip validation entirely. A validator receives the value and, since 3.4, the resolved props as a second argument.

open as a page