In a Vue 3 `<script setup>` component, how do you declare props with defineProps, and how do runtime and type-based declarations differ?
answer
- compiler macro, no import needed
- array, object, or type argument
- type, required, default, validator
- types compiled into runtime options
- one argument form per call
basics
~20 sdefineProps 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<script setup lang="ts">
const props = defineProps<{
userId: number
label?: string
}>()
</script>
<template>
<h2>{{ label ?? 'Profile' }} #{{ props.userId }}</h2>
</template>go deeper
Recall the three runtime shapes and the type-based one, and say that runtime props are optional unless required is true.
Explain that defineProps is a compiler macro that emits the props option, and how optional keys in the type map to required: false.
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.
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