feat: Complete fleet — 94 skills across 10+ domains
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>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
---
|
||||
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/)
|
||||
Reference in New Issue
Block a user