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>
5.4 KiB
5.4 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||
|---|---|---|---|---|---|---|---|---|---|
| Boolean Props Default to false, Not undefined | MEDIUM | TypeScript expects optional boolean to be undefined but Vue defaults it to false, causing type confusion | gotcha |
|
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
undefinedis a meaningful state - Document this Vue-specific behavior for your team
The Gotcha
<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:
<!-- 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: 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:
<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":
<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"
<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:
// Order matters in runtime declaration!
defineProps({
// Boolean first: empty string becomes true
disabled: [Boolean, String]
})
// <MyComponent disabled /> → disabled = true
// <MyComponent disabled="" /> → disabled = true
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
<!-- 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:
<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:
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