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?
answer
- a type argument on a macro
- the question mark means optional
- runtime props generated from the type
- never both a type and an object
basics
~20 sPass 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 sIn `<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<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
Recall the defineProps<T>() form, that ? marks an optional prop, and that the macro needs no import.
Explain that the compiler generates runtime props from the type, how types map to constructors, and why both forms cannot be mixed.
Know what survives into production builds and why prop keys and Boolean types must remain there.
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.