AcademyVue
Vue with TypeScript: Props & Emits
A discriminated union prop or a typed emits object only pays off once the component actually branches and matches on it.
2 min readUpdated 3 exercises
A discriminated union describes props that vary together
Some components accept genuinely different sets of props depending on one tag field — a button that is either a link or an action, never a mix of both:
type ButtonSpec =
| { variant: 'link'; href: string; label: string }
| { variant: 'action'; onClick: () => void; label: string }In a real editor, narrowing on spec.variant === 'link' is what lets TypeScript know href exists in that branch and onClick does not — the type only helps once the code actually checks the tag. Declaring the union and then ignoring variant entirely compiles fine (Sandpack does not even type-check it) and produces a component that always behaves like one branch, regardless of what was passed.
// The type is correct; the component never reads it
return () => h('button', { onClick: props.spec.onClick }, props.spec.label)
// Reading the tag is what makes the union do anything
return props.spec.variant === 'link'
? h('a', { href: props.spec.href }, props.spec.label)
: h('button', { onClick: props.spec.onClick }, props.spec.label)The object form of emits validates a name and a payload shape
const emit = defineEmits<{
search: [value: string]
}>()This is the <script setup> equivalent of the object-form emits option — a specific event name (search) with a specific payload type ([value: string], a tuple of arguments). The array form, emits: ['search'], is a name list with no payload contract at all: emitting emit('search', 42) when the parent expects a string is caught nowhere.
The more common mistake is not the payload — it is the name itself. emits: ['search'] declared, but emit('update', value) called from inside the component: the parent's @search listener hears nothing, because the string that was actually emitted was never 'search' to begin with. Nothing enforces this at runtime; the object form at least gives a real editor's type checker a chance to catch it before the code ships.
Sandpack transpiles TypeScript; it does not type-check it
Every example above compiles and runs identically whether the types are right or completely wrong, inside this platform's Sandpack workspace. The types are for the editor you write real Vue code in — catching exactly the branch-not-taken and event-name-mismatch bugs above before they reach a browser at all. Treat a TypeScript pattern here as documentation of intent that a real toolchain enforces, not as something that changes behavior on its own.
What interviewers check
Whether a discriminated union prop type gets branched on, not just declared — declaring ButtonSpec and then writing props.spec.onClick unconditionally is a very common half-implementation, and naming why it is incomplete is usually the more revealing question than writing the union type itself.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- Typed PropsProA correctly-typed discriminated union that the component never branches on — always renders the wrong element.DrillSolve · HardDrillSolve · Hard
- Typed EmitsProUntyped array-syntax emits let a mismatched event name ship silently — the object form is what would have caught it.DrillSolve · HardDrillSolve · Hard
- Composition API · watch & ComposablesTyped props and emits sit on top of the same Composition API fundamentals this set checks.QuizTake · MediumQuizTake · Medium
Read next
Vue with TypeScript: Generics & Type Guards
A generic <T> only constrains what a function accepts — a type guard is the one construct that actually checks a value's shape at runtime.
2 min
Props & Component Events
Props flow down, events flow up — and the object syntax for both is what actually gives you defaults, validation and v-model.
2 min