Skip to content
← All questions
⚡ Intermediate

How does withDefaults work and what are its gotchas?

🔷 TypeScript🧩 Components

withDefaults provides default values for type-based defineProps. The main gotcha: mutable defaults (arrays, objects) MUST use factory functions, otherwise all component instances share the same reference. Vue 3.5+ introduces destructured defaults that handle this automatically.

Basic usage ​

vue
<script setup lang="ts">
interface Props {
  title?: string
  count?: number
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  title: 'Untitled',
  count: 0,
  items: () => []
})
</script>
Open in Vue Playground

withDefaults only applies to optional props (the ones with ?). Required props don't need defaults.

The mutable default problem ​

Arrays and objects are passed by reference. Without a factory function, every instance of the component gets the same object:

vue
<script setup lang="ts">
// BUG: all instances share the same array
const props = withDefaults(defineProps<{ tags?: string[] }>(), {
  tags: ['default']
})
</script>
Open in Vue Playground

If one component instance mutates tags, every other instance sees the change. This causes bugs like "selecting a row in one table selects it in all tables."

The fix: factory functions ​

Wrap mutable defaults in an arrow function so each instance gets a fresh copy:

vue
<script setup lang="ts">
interface Props {
  title?: string
  disabled?: boolean
  items?: string[]
  config?: { theme: string; locale: string }
  selectedIds?: Set<string>
}

const props = withDefaults(defineProps<Props>(), {
  title: 'Default', // primitive — no factory needed
  disabled: false, // primitive — no factory needed
  items: () => [], // array — factory required
  config: () => ({
    // object — factory required
    theme: 'light',
    locale: 'en'
  }),
  selectedIds: () => new Set() // Set — factory required
})
</script>
Open in Vue Playground

When you need a factory function ​

TypeFactory neededDefault syntax
stringNo'hello'
numberNo42
booleanNofalse
nullNonull
ArrayYes() => []
ObjectYes() => ({})
Map / SetYes() => new Map()
DateYes() => new Date()

The rule: if typeof value === 'object', use a factory.

Vue 3.5+: destructured defaults ​

Vue 3.5 introduced reactive props destructuring. Defaults are written with standard JavaScript destructuring syntax, and Vue handles instance isolation automatically:

vue
<script setup lang="ts">
const {
  title = 'Untitled',
  count = 0,
  items = ['default'],
  config = { theme: 'light' }
} = defineProps<{
  title?: string
  count?: number
  items?: string[]
  config?: { theme: string }
}>()
</script>
Open in Vue Playground

No withDefaults, no factory functions. Each component instance gets its own copy. This is the recommended approach on Vue 3.5+.

withDefaults vs destructured defaults ​

vue
<!-- Vue 3.4 and below: withDefaults -->
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    items?: string[]
    label?: string
  }>(),
  {
    items: () => [],
    label: 'Default'
  }
)
// access: props.items, props.label
</script>

<!-- Vue 3.5+: destructured defaults -->
<script setup lang="ts">
const { items = [], label = 'Default' } = defineProps<{
  items?: string[]
  label?: string
}>()
// access: items, label (directly, no 'props.' prefix)
</script>
Open in Vue Playground

Both approaches are valid. withDefaults gives you a single props object. Destructuring gives you individual variables. On Vue 3.5+, destructuring is simpler and avoids the factory function footgun entirely.

See also: How do you declare props with TypeScript? · What are the limitations of defineProps types?

References ​

Released under the MIT License.