skip to content

Typing Props & Emits

Declaring props and events with types: defineProps<T>(), withDefaults vs destructure defaults, imported types in macros, and defineEmits signatures. Interviewers probe runtime vs type-only.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Vue 3.5 `<script setup lang="ts">` component, how do you declare props with a TypeScript type, and what does the compiler generate from it?

level: juniorimportance: must knowfreq 70%

answer

  1. a type argument on a macro
  2. the question mark means optional
  3. runtime props generated from the type
  4. never both a type and an object

basics

~20 s

Pass a type argument to defineProps, for example defineProps<{ options: Option[]; selected?: string }>(). The SFC compiler reads that type and generates the runtime props declaration: non-optional props become required, and constructors such as Array and String are inferred.

solid answer

~40 s

In `<script setup lang="ts">` you call `defineProps<Props>()` with a type literal or interface and no runtime argument. It is a compiler macro, so it needs no import. The SFC compiler analyses the type and writes the runtime `props` option for you: in a development build `options: Option[]` becomes `{ type: Array, required: true }` and `selected?: string` becomes `{ type: String, required: false }`; a production build drops the type checks and keeps only what changes behaviour, such as `Boolean` types for casting and defaults. The returned `props` object is typed readonly from the same type, and `vue-tsc` checks every parent usage against it. You may use a type argument or a runtime object, never both: passing both is a compile error.

code

vue · 15 lines
vue
<script setup lang="ts">
import SelectInput from './SelectInput.vue'

const countries = [
  { value: 'de', label: 'Germany' },
  { value: 'fr', label: 'France' }
]
</script>

<template>
  <!-- ok -->
  <SelectInput :options="countries" selected="de" />
  <!-- vue-tsc error: missing required prop 'options' -->
  <SelectInput placeholder="Pick one" />
</template>

go deeper

for a junior

Recall the defineProps<T>() form, that ? marks an optional prop, and that the macro needs no import.

for a middle

Explain that the compiler generates runtime props from the type, how types map to constructors, and why both forms cannot be mixed.

for a senior

Know what survives into production builds and why prop keys and Boolean types must remain there.

for a principal

Standardise type-based props across a codebase while deciding where runtime validation of untrusted input is still required.

## Two ways to declare props A Vue component's **props** are its inputs. In `<script setup>` they are declared with the **`defineProps`** compiler macro, in one of two forms: - **Runtime declaration**: `defineProps({ selected: String })`, the same value the Options API `props` option takes. - **Type-based declaration**: `defineProps<{ selected?: string }>()`, a TypeScript type argument and no runtime argument. The type-based form is the default choice in TypeScript projects because the type is written once and serves both the editor and the runtime. ## A typed SelectInput ```vue <script setup lang="ts"> interface Option { value: string label: string disabled?: boolean } const props = defineProps<{ options: Option[] selected?: string placeholder?: string }>() </script> <template> <select :value="props.selected"> <option v-if="props.placeholder" value="" disabled>{{ props.placeholder }}</option> <option v-for="o in props.options" :key="o.value" :value="o.value" :disabled="o.disabled"> {{ o.label }} </option> </select> </template> ``` ## What the compiler generates The browser cannot read TypeScript types, and Vue still needs to know at runtime which attributes are props. So the SFC compiler **statically analyses** the type and emits an equivalent runtime declaration. Each property's `?` decides `required`, and its type maps to a runtime constructor: | TypeScript type | Runtime `type` | |---|---| | `string`, or `'a' \| 'b'` | `String` | | `number` | `Number` | | `boolean` | `Boolean` | | `Option[]` or a tuple | `Array` | | An interface or object type | `Object` | | A function type | `Function` | | `string \| number` | `[String, Number]` | For the SelectInput, a development build therefore contains roughly `options: { type: Array, required: true }` and `selected: { type: String, required: false }`. ## Development versus production output 1. **Development builds** keep full entries (`type`, `required`, defaults), so Vue can warn, for example `Missing required prop: "options"`. 2. **Production builds** drop the type checks, since prop warnings are development-only anyway. The compiler keeps each prop **key**, so Vue still separates props from fallthrough attributes, and keeps only what changes behaviour: `Boolean` types (needed for boolean casting), defaults, and `Function` types in some default cases. ## What the type buys you - The returned `props` is typed `Readonly<...>` from your type, so `props.options` is `Option[]` and assigning to it is a type error. - **Parents are checked**: `vue-tsc` and the editor report `<SelectInput />` without `options`, or `:options="['a']"`, as template type errors. - The **same type** documents the component's API; there is no second runtime declaration to drift out of sync. ## Reading props in the template and in script Inside the template, props are available by name, so `{{ placeholder }}` and `props.placeholder` both work; the example above uses `props.` only for clarity. In script code you read them through the object returned by `defineProps` (or through destructured variables in Vue 3.5). Either way the types come from the same declaration, so `o.label` inside the `v-for` is a `string` and a typo such as `o.lable` is a template type error. ## When the runtime form still fits The type-based form is the default for TypeScript code, but the runtime object form remains useful: - when a prop needs a **`validator`** function, which a type cannot express; - in **plain JavaScript** components, where there is no type to analyse; - when a props type is too complex for the compiler to resolve, in which case `defineProps({ options: { type: Array as PropType<Option[]>, required: true } })` gives the same TypeScript type with an explicit runtime declaration. Most teams mix the two only at that level: type-based everywhere, runtime where validation or resolution demands it. ## Rules and pitfalls - **Type or runtime, never both**: `defineProps<{ a: string }>({ a: String })` fails with `defineProps() cannot accept both type and non-type arguments at the same time`. - **Only `?` makes a prop optional**: `selected: string | undefined` without `?` is still generated as `required: true`. - **Values are not validated against the TypeScript type** at runtime: only the coarse constructor check above runs, and only in development. - **One call per component**: a second `defineProps` is a compile error. - **Complex types have limits**: the conversion reads the type's syntax, so some computed types cannot be turned into runtime props.

  • In a Vue type-based defineProps, is `selected: string | undefined` an optional prop?
    No. The compiler decides `required` from the `?` modifier only, so a property without `?` is generated as `required: true` even if its type includes `undefined`. Parents that omit it get a type error and, in development, a missing-prop warning. Write `selected?: string` for an optional prop.
  • Why does a Vue production build keep an entry for every type-based prop even though it drops the type checks?
    Vue must still know which attributes are props and which are fallthrough attributes, and some entries change behaviour: a `Boolean` type drives boolean casting and a default supplies missing values. So each key stays, with `type` kept only where it matters.

saying these in an interview costs you the question

  • Type-based props exist only in TypeScript and have no runtime declaration at all.
  • You can pass a type argument and a runtime object to defineProps together.
  • Adding | undefined to a prop's type makes it optional.
  • Vue validates prop values against the full TypeScript type at runtime.
  • defineProps must be imported from vue before use.
open as a page

In Vue 3.5, how do `withDefaults` and destructured defaults differ for type-based props such as a SelectInput's `options?: Option[]`?

level: middleimportance: must knowfreq 55%

basics

~20 s

withDefaults(defineProps<Props>(), { options: () => [] }) needs factory functions for mutable defaults. Since 3.5, destructuring, const { options = [] } = defineProps<Props>(), lets the compiler add the factory. Both compile to runtime defaults.

open as a page

In a Vue 3.3+ `<script setup lang="ts">` component, what are the two type syntaxes for defineEmits, and what reaches the runtime?

level: middleimportance: should knowfreq 50%

basics

~20 s

Either call signatures, { (e: 'change', value: string): void }, or since 3.3 named tuples, { change: [value: string] }. Both type emit() calls and parent handlers the same way; at runtime the compiler emits only the list of event names.

open as a page

A Vue 3.5 component declares `size?: 'sm' | 'md' | 'lg'` and `disabled?: boolean` with type-based defineProps; what does Vue actually check and change at runtime?

level: seniorimportance: should knowfreq 35%

basics

~20 s

At runtime size is only checked as a String, and only in development, so 'xl' passes silently; the literal union is enforced only by vue-tsc. disabled gets Boolean casting: absent means false, and its type is boolean, not optional.

open as a page

A Vue 3.5 build fails with "Unresolvable type reference or unsupported built-in utility type" on `defineProps<Props>()`; what causes it, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The SFC compiler builds runtime props by reading the type's syntax, not by running TypeScript, so only interfaces, type literals and a few utility types resolve. Rewrite the props type into such a shape or use a runtime declaration.

open as a page