codejump

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

Waiting at the end of this page.

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:

TypeScript
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.

TypeScript
// 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

TypeScript
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 drills

Reading this page is the cheap half. These are the exercises that make you use it.