Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
5.4 KiB
Markdown
226 lines
5.4 KiB
Markdown
---
|
|
title: Boolean Props Default to false, Not undefined
|
|
impact: MEDIUM
|
|
impactDescription: TypeScript expects optional boolean to be undefined but Vue defaults it to false, causing type confusion
|
|
type: gotcha
|
|
tags: [vue3, typescript, props, boolean, defineProps]
|
|
---
|
|
|
|
# Boolean Props Default to false, Not undefined
|
|
|
|
**Impact: MEDIUM** - When using type-based `defineProps`, optional boolean props (marked with `?`) behave differently than TypeScript expects. Vue treats boolean props specially: an absent boolean prop defaults to `false`, not `undefined`. This can cause confusion when TypeScript thinks the type is `boolean | undefined`.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Understand that Vue's boolean casting makes absent booleans `false`
|
|
- [ ] Use `withDefaults()` to be explicit about boolean defaults
|
|
- [ ] Consider using non-boolean types if `undefined` is a meaningful state
|
|
- [ ] Document this Vue-specific behavior for your team
|
|
|
|
## The Gotcha
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
disabled?: boolean // TypeScript sees: boolean | undefined
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
// TypeScript thinks props.disabled could be undefined
|
|
if (props.disabled === undefined) {
|
|
console.log('This will NEVER run!')
|
|
// Vue's boolean casting means disabled is false, not undefined
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- When used without the prop -->
|
|
<MyComponent />
|
|
<!-- disabled is false, NOT undefined -->
|
|
</template>
|
|
```
|
|
|
|
## Why This Happens
|
|
|
|
Vue has special "boolean casting" behavior inherited from HTML boolean attributes:
|
|
|
|
```vue
|
|
<!-- All of these make disabled = true -->
|
|
<MyComponent disabled />
|
|
<MyComponent :disabled="true" />
|
|
<MyComponent disabled="" />
|
|
|
|
<!-- This makes disabled = false (NOT undefined) -->
|
|
<MyComponent />
|
|
|
|
<!-- Explicit false -->
|
|
<MyComponent :disabled="false" />
|
|
```
|
|
|
|
This is by design to match how HTML works:
|
|
```html
|
|
<!-- HTML: presence means true, absence means false -->
|
|
<button disabled>Can't click</button>
|
|
<button>Can click</button>
|
|
```
|
|
|
|
## Solutions
|
|
|
|
### Solution 1: Be Explicit with withDefaults
|
|
|
|
Make your intention clear:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
disabled?: boolean
|
|
}
|
|
|
|
// Explicitly document the default
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
disabled: false // Now it's clear this defaults to false
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### Solution 2: Use a Three-State Type
|
|
|
|
If you actually need to distinguish "not set" from "explicitly false":
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
// Use a union type instead of optional boolean
|
|
state?: 'enabled' | 'disabled' | undefined
|
|
|
|
// Or use undefined explicitly
|
|
toggleState?: boolean | undefined
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
state: undefined, // Can actually be undefined
|
|
toggleState: undefined
|
|
})
|
|
|
|
// Now you can check for undefined
|
|
if (props.state === undefined) {
|
|
// Use parent's state
|
|
} else if (props.state === 'disabled') {
|
|
// Explicitly disabled
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Solution 3: Use null for "Not Set"
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
// null = not set, false = explicitly off, true = explicitly on
|
|
selected: boolean | null
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
selected: null
|
|
})
|
|
|
|
// Three distinct states
|
|
if (props.selected === null) {
|
|
console.log('Selection not specified')
|
|
} else if (props.selected) {
|
|
console.log('Selected')
|
|
} else {
|
|
console.log('Explicitly not selected')
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## Boolean Casting Order
|
|
|
|
Vue also has special behavior when Boolean and String are both valid:
|
|
|
|
```typescript
|
|
// Order matters in runtime declaration!
|
|
defineProps({
|
|
// Boolean first: empty string becomes true
|
|
disabled: [Boolean, String]
|
|
})
|
|
|
|
// <MyComponent disabled /> → disabled = true
|
|
// <MyComponent disabled="" /> → disabled = true
|
|
```
|
|
|
|
```typescript
|
|
defineProps({
|
|
// String first: empty string stays as string
|
|
disabled: [String, Boolean]
|
|
})
|
|
|
|
// <MyComponent disabled /> → disabled = ''
|
|
// <MyComponent disabled="" /> → disabled = ''
|
|
```
|
|
|
|
With type-based declaration, Boolean always takes priority for absent props.
|
|
|
|
## Common Bug Pattern
|
|
|
|
```vue
|
|
<!-- Parent.vue -->
|
|
<script setup lang="ts">
|
|
const userPreferences = ref({
|
|
darkMode: undefined as boolean | undefined
|
|
})
|
|
|
|
// Fetch preferences...
|
|
onMounted(async () => {
|
|
userPreferences.value = await fetchPreferences()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Bug: undefined becomes false, not "inherit system preference" -->
|
|
<ThemeToggle :darkMode="userPreferences.darkMode" />
|
|
</template>
|
|
```
|
|
|
|
**Fix:**
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const userPreferences = ref<{
|
|
darkMode: boolean | null
|
|
}>({
|
|
darkMode: null // Use null for "not yet loaded"
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Now ThemeToggle can distinguish between null and false -->
|
|
<ThemeToggle :darkMode="userPreferences.darkMode" />
|
|
</template>
|
|
```
|
|
|
|
## TypeScript Type Accuracy
|
|
|
|
The Vue type system handles this, but it can be confusing:
|
|
|
|
```typescript
|
|
interface Props {
|
|
disabled?: boolean
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
// At compile time: boolean | undefined
|
|
// At runtime: boolean (never undefined due to Vue's boolean casting)
|
|
|
|
// TypeScript is technically "wrong" here, but the withDefaults usage
|
|
// or explicit false default can help align expectations
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js Props - Boolean Casting](https://vuejs.org/guide/components/props.html#boolean-casting)
|
|
- [GitHub Issue: Boolean props default to false](https://github.com/vuejs/core/issues/8576)
|
|
- [TypeScript Vue 3 Props](https://madewithlove.com/blog/typescript-vue-3-and-strongly-typed-props/)
|