In Vue 3.5, how do `withDefaults` and destructured defaults differ for type-based props such as a SelectInput's `options?: Option[]`?
answer
- a second macro wrapping defineProps
- native default syntax since 3.5
- mutable defaults need a factory
- the compiler adds the factory for you
- do not combine the two
basics
~20 swithDefaults(defineProps<Props>(), { options: () => [] }) needs factory functions for mutable defaults. Since 3.5, destructuring, const { options = [] } = defineProps<Props>(), lets the compiler add the factory. Both compile to runtime defaults.
solid answer
~50 sType-based props cannot carry defaults inside the type, so Vue offers two ways. `withDefaults(defineProps<Props>(), { placeholder: 'Select...', options: () => [] })` compiles its second argument into runtime `default` options, type-checks the defaults and removes the optional flag from defaulted props on the returned type; array and object defaults must be written as functions so each instance gets its own copy. Since Vue 3.5, reactive props destructure is on by default, so `const { placeholder = 'Select...', options = [] } = defineProps<Props>()` does the same with native syntax: the compiler moves each default into the runtime declaration and wraps non-literal defaults in a factory itself, and it reports a best-effort mismatch between default and type. Do not combine them: destructuring a `withDefaults` result triggers a compiler warning and disables reactive destructure. On 3.4 and below, `withDefaults` was the way.
go deeper
Recall that type-based props get defaults through withDefaults or, in Vue 3.5, destructure default values.
Explain why withDefaults needs factories for arrays and objects and why destructure defaults do not.
Spot mixed styles and shared mutable defaults in review, and migrate withDefaults code to destructure defaults safely.
Pick one defaults style for the codebase, weighing a props object against local variables and the version floor of shared libraries.
## Why defaults need special syntax A TypeScript type cannot hold values, so `defineProps<{ placeholder?: string }>()` has nowhere to put a default placeholder. Vue has two answers for type-based props, and which one a codebase uses usually depends on when it was written. ```ts interface Option { value: string; label: string } interface Props { options?: Option[] placeholder?: string searchable?: boolean } ``` ## Option 1: `withDefaults` ```ts const props = withDefaults(defineProps<Props>(), { options: () => [], placeholder: 'Select...', searchable: false }) ``` - It is a **compiler macro** taking the `defineProps` call as its first argument; the second argument is required. - The defaults are compiled into the runtime props' **`default`** options. - The defaults are **type-checked** against `Props`. - The returned `props` type **drops the optional flag** for defaulted props: `props.placeholder` is `string`, not `string | undefined`. - **Mutable defaults must be factory functions** (`() => []`): otherwise every instance would share one array, and a mutation in one select would appear in all of them. - It works only with **type-based** `defineProps`; the compiler rejects it around a runtime declaration. ## Option 2: destructure defaults (3.5+) ```ts const { options = [], placeholder = 'Select...', searchable = false } = defineProps<Props>() ``` - Since **Vue 3.5**, reactive props destructure is enabled by default, so native JavaScript default syntax declares prop defaults. - The compiler moves each default into the runtime declaration. For a **non-literal** default such as `[]`, it generates a factory `() => ([])` itself, so **no manual wrapper** is needed and each instance still gets its own array. - TypeScript's own destructuring rules make `options` an `Option[]` without `undefined`. - The compiler runs a best-effort check and fails with `Default value of prop "x" does not match declared type.` when, say, a string default is given to a `number` prop. - Code in the same `<script setup>` that reads `options` is compiled to `props.options`, which is what keeps it reactive. ## Side by side | Aspect | `withDefaults` | Destructure defaults | |---|---|---| | Available | Vue 3.2 and later | Default since Vue 3.5 | | Array or object default | Must be `() => []` | Plain `[]`; compiler wraps it | | Result | A `props` object | Local variables | | Type of defaulted prop | Optional flag removed | Non-undefined via destructuring | | Mismatch check | Type-checked by TypeScript | Best-effort compiler check | ## Do not combine them ```ts // compiler warning const { options } = withDefaults(defineProps<Props>(), { options: () => [] }) ``` The compiler warns that `withDefaults()` is unnecessary when destructuring, that **reactive destructure is disabled** when using it, and suggests destructure defaults instead. Pick one style per component; for new 3.5 code, destructure defaults are the documented recommendation. ## Migrating a component to destructure defaults Moving a 3.5 component from `withDefaults` to destructure defaults is mechanical, but each step has a trap: 1. **Move each default into the destructuring pattern**, unwrapping factories: `options: () => []` becomes `options = []`, because the compiler now adds the factory. 2. **Keep function-typed defaults as functions**: for a prop typed as a function, the default value itself is the function, and the compiler does not wrap it again. 3. **Replace `props.x` reads with the local names**, or keep a separate `props` object if the template or script relies on it; the destructured names are compiled back into `props.x` accesses. 4. **Check code that passes a prop onward**, for example into `watch` or a composable; destructured props follow their own reactivity rules there, so review those call sites rather than assuming the rewrite is purely cosmetic. 5. **Run `vue-tsc`**: the types of defaulted props stay non-optional, so consumers should see no change. ## Pitfalls 1. Writing `options: []` inside `withDefaults`: one shared array for every instance. 2. Adding `() => []` inside a destructure default: the default becomes a function value, not an array. 3. Assuming destructure works the same on Vue 3.4 and below, where it was not enabled by default. 4. Assigning to a destructured prop: props are readonly, and the compiler rejects it.
- What does the Vue 3.5 compiler generate for the destructure default `options = []`?A runtime prop entry whose `default` is a factory, `() => ([])`, because `[]` is not a literal primitive. Each component instance therefore gets a fresh array, which is why destructure defaults need no manual function wrapper while `withDefaults` does.
- In Vue 3.5, what does `const { size = 'md' } = defineProps<{ size?: number }>()` do at compile time?The compiler's best-effort check sees a string default for a prop whose inferred runtime type is `Number` and fails with `Default value of prop "size" does not match declared type.` That check matters because TypeScript alone would accept the destructure and simply widen `size` to `number | string`.
saying these in an interview costs you the question
- withDefaults accepts plain [] and {} defaults safely because Vue clones them.
- Destructure defaults need () => [] exactly like withDefaults does.
- Destructuring the result of withDefaults is the best of both styles.
- Destructure defaults only affect the TypeScript type, not the runtime props.
- Reactive props destructure has been on by default since Vue 3.0.