Props are like parameters in functions. Use props to customize components.
React's design philosophy is "props are just function arguments."
Vue requires you to explicitly declare which props your component accepts using defineProps().
Props are reactive.
Vue props are deeply immutable from the child's perspective. Children should emit events back to the parent.
interface Props { title: string isPublished?: boolean publishedAt?: Date}export default function ArticleCard({ title, isPublished, publishedAt,}: Props) { return ( <> <h1>{title}{!isPublished && ' (draft)'}</h1> { publishedAt ? <span>{publishedAt.toString()}</span> : undefined } </> )}<ArticleCard title="React" publishedAt={new Date()} isPublished/><script setup lang="ts">import { defineProps } from 'vue';const { title, publishedAt, isPublished } = defineProps<{ title: string; isPublished?: boolean; publishedAt?: Date;}>();</script><template> <h1>{{ title }}{{ !isPublished && ' (draft)' }}</h1> <span v-if="publishedAt">{{ publishedAt }}</span></template><ArticleCard title="Vue" :publishedAt="new Date()" isPublished/>export default function MyComponent({ greeting = 'Hello', isAdmin = false, user = { name: 'Guest' },}: { greeting?: string isAdmin?: boolean user?: { name: string }} = {}) { return ( <div> <p>{greeting}</p> <p>Admin: {isAdmin ? 'Yes' : 'No'}</p> <p>User: {user.name}</p> </div> )}<script setup lang="ts">const { greeting = 'Hello', isAdmin = false, user = () => ({ name: 'Guest' }),} = defineProps<{ greeting?: string; isAdmin?: boolean; user?: { name: string };}>();</script><template> <div> <p>{{ greeting }}</p> <p>Admin: {{ isAdmin ? 'Yes' : 'No' }}</p> <p>User: {{ user.name }}</p> </div></template>interface Props { disabled?: boolean}Usage
<Button disabled /> <!-- disabled = true --><Button disabled={false} /> <!-- disabled = false --><Button disabled={true} /> <!-- disabled = true --><Button /> <!-- disabled = undefined -->// ❌ Wrong - won't coerce<Button disabled="true" /> // (string, not boolean!)// ✅ Correct<Button disabled={true} /> // boolean<script setup lang="ts">const { disabled, size } = defineProps<{ disabled?: boolean; size: boolean | string;}>();</script>Usage
<Button /> <!-- disabled = false --><Button :disabled="false" /> <!-- disabled = false --><Button disabled /> <!-- disabled = true --><Button size /> <!-- size = true (casting) --><Button size="large" /> <!-- size = "large" (string) -->The Boolean absent props will be cast to false.
An absent optional prop other than Boolean will have undefined value.
export default function MyComponent({ start, disabled = false, description,}: { start: number; disabled?: boolean; description?: string | null;}) { return (...)}const { start, disabled = false, description,} = defineProps<{ start: number; disabled?: boolean; description?: string | null;}>();The Boolean absent props will be cast to false.
This pattern prevents developers from passing invalid prop combinations.
There are used discriminated unions with TypeScript. A discriminant (usually a literal type like variant: 'text') determines which other properties are valid. You use the never type to explicitly forbid properties in certain branches.
type Props = | { variant: 'text'; icon?: never; label: string; } | { variant: 'icon'; icon: React.ReactNode; label?: never; };export default function Button({ variant, label, icon }: Props) { <button>{variant === 'text' ? label : icon}</button>;}Usage:
// ✅ <Button variant="text" label="Click me" />// ✅ <Button variant="icon" icon={<Icon />} />// ❌ <Button variant="text" icon={<Icon />} /> // TypeScript error!// ❌ <Button variant="icon" label="Click me" /> // TypeScript error!<script setup lang="ts">type Props = | { variant: 'text'; icon?: never; label: string; } | { variant: 'icon'; icon: any; label?: never; };const { variant, label, icon } = defineProps<Props>();</script><template> <button> {{ variant === 'text' ? label : icon }} </button></template>type Status = 'draft' | 'published' | 'archived'interface Props { title: string content: string | number status?: Status rating?: number}const validateProps = ({ status, rating }: Props) => { const errors: string[] = [] if (status !== undefined) { if (!['draft', 'published', 'archived'].includes(status)) { errors.push( `Invalid status: "${status}". Must be one of: draft, published, archived` ) } } if (rating < 0 || rating > 5) { errors.push(`Rating: ${rating} must be between 0 and 5`) } if (errors.length > 0) { console.warn('[Props Validation]', errors.join('\n')) if (process.env.NODE_ENV === 'development') { throw new Error(errors.join('\n')) } }}export default function BlogPost({ title, content, status = 'published', rating,}: Props) { validateProps({ title, content, status, rating }) return (...)}<script setup lang="ts">type Status = 'draft' | 'published' | 'archived';defineProps({ title: { type: String, // Basic type check required: true, }, content: { type: [String, Number], // Multiple possible types required: true, }, status: { type: PropType<Status>, default: 'published', validator: (value: Status) => { return ['draft', 'published', 'archived'].includes(value); }, }, rating: { type: Number, validator: (value: number) => { return value >= 0 && value <= 5; }, },});</script>When prop validation fails, Vue will produce a console warning (if using the development build).