skip to content

In Vue 3's <script setup>, why are defineProps, defineEmits and the other define* macros never imported, and what restrictions follow from that?

level: middleimportance: must knowfreq 58%

answer

  1. the compiler, not the runtime
  2. arguments leave the setup scope
  3. imports yes, locals no
  4. no aliasing, one call each

basics

~20 s

The define* macros are compiler hints that @vue/compiler-sfc recognises and compiles away, so they need no import. Their arguments are hoisted to module scope, so they cannot reference variables declared in <script setup>, and they only work inside <script setup>.

solid answer

~40 s

`defineProps`, `defineEmits`, `defineModel` (3.4+), `defineExpose`, `defineOptions` and `defineSlots` (3.3+), plus `withDefaults`, are **compiler macros**. The SFC compiler finds the calls in `<script setup>` and replaces them with component options and generated code, so there is nothing to import. Three restrictions follow. First, their **arguments are hoisted** out of `setup()` into module scope, so they can reference imports but not variables declared in the block; that is a compile error. Second, they **only work in `<script setup>`**: called in a normal script or a composable, the runtime stub only warns in development that it is a compiler-hint helper whose arguments have no effect. Third, the compiler matches them **by name**: importing one under an alias is a compile error, and `defineOptions` refuses props, emits, expose and slots, which have their own macros.

code

vue · 22 lines
vue
<script setup lang="ts">
import { MAX_ITEMS } from './limits'

const fallbackLabel = 'Untitled'

// OK: an imported binding lives in module scope too
const props = defineProps({
  limit: { type: Number, default: MAX_ITEMS },
})

// Compile error: the options are hoisted outside setup(),
// where a setup-scope variable does not exist
// const props = defineProps({
//   label: { type: String, default: () => someLocalRef.value },
// })

defineOptions({ inheritAttrs: false })
</script>

<template>
  <p>{{ props.limit }} {{ fallbackLabel }}</p>
</template>

go deeper

for a junior

Recall that the define* macros need no import, work only in <script setup>, and include defineProps, defineEmits, defineModel, defineExpose, defineOptions and defineSlots.

for a middle

Explain that the compiler erases the calls and hoists their arguments into module scope, which is why imports are allowed and setup-scope variables are not.

for a senior

Diagnose macro misuse quickly: calls in composables, aliased imports, options depending on local state, and props smuggled into defineOptions.

for a principal

Weigh the ergonomics of compile-time macros against their non-function semantics when setting team conventions and lint rules for SFC code.

## Macros, not functions A **compiler macro** is a call that looks like a function call in your source but is consumed by the compiler. In a Vue single-file component, `@vue/compiler-sfc` scans `<script setup>` for these names and rewrites them into component options before any code runs. | Macro | Available since | What the compiler produces from it | |---|---|---| | `defineProps` | stable `<script setup>` (3.2) | the `props` option | | `defineEmits` | stable `<script setup>` (3.2) | the `emits` option | | `withDefaults` | stable `<script setup>` (3.2) | prop `default` values for type-based props | | `defineExpose` | stable `<script setup>` (3.2) | a call to the setup context's `expose()` | | `defineOptions` | 3.3 | other component options, such as `name` or `inheritAttrs` | | `defineSlots` | 3.3 | slot typing; returns the `slots` object | | `defineModel` | 3.4 | a model prop, its update event and a ref | What each macro declares is a separate topic; this answer is about what all of them share because they are macros. Because the compiler recognises them syntactically and removes them, they are available in `<script setup>` **without an import**. Editors know their types through Vue's type declarations. ## Restriction 1: arguments are hoisted out of `setup()` The options you pass become part of the component definition, which lives at **module scope**, outside the per-instance `setup()` function. Code in `<script setup>` only exists inside `setup()`. So: - An argument may reference **imported bindings**, which are module-scoped too. - It may not reference **variables declared in `<script setup>`**. The compiler rejects it with an error explaining that the macro cannot reference locally declared variables because it will be hoisted outside of the `setup()` function, and suggesting a normal `<script>` for module-scope initialisation. - Simple literal constants are tolerated in some cases, and the `defineOptions` docs mention that exception, but relying on it is fragile; move shared constants into an import or a normal `<script>`. ## Restriction 2: only inside `<script setup>` The names also exist as runtime exports of `vue`, but only as stubs. If a macro reaches the runtime, for example because it was called in a normal `<script>` or inside a composable in a `.ts` file, nothing is declared. In development it warns that the helper is a compiler hint only usable inside `<script setup>` of a single file component and that passing it at runtime has no effect. A composable that needs props must receive them as an argument. ## Restriction 3: matched by name and shape 1. **No aliasing.** `import { defineProps as props } from 'vue'` is a compile error: the compiler needs the canonical name to find the call. 2. **One call each.** A second `defineOptions()` or `defineSlots()` in the same block is reported as a duplicate. 3. **Each option has one home.** `defineOptions()` errors if you pass `props`, `emits`, `expose` or `slots`; use the dedicated macro instead. 4. **Shape rules.** `defineSlots()` takes a type parameter only and no runtime arguments; `defineOptions()` takes no type arguments and returns nothing, so assigning its result is an error. ## What the output looks like Given `const bar = 1` and `const props = defineProps({ foo: String })` in `<script setup>`, the compiler's own test snapshot for a separately compiled template shows roughly this output: ```js const bar = 1 export default { props: { foo: String }, setup(__props, { expose: __expose }) { __expose(); const props = __props return { props, bar } } } ``` Three things are visible: the macro call has vanished and its argument has become the `props` option **outside** `setup()`; the local `props` is simply the incoming `__props`; and the literal constant `bar` was hoisted to module scope, which is why literal constants are the one kind of local binding a macro argument can sometimes reference. ## Why this design - **Type inference**: a type argument such as `defineProps<{ id: number }>()` can be turned into runtime options at compile time, which a plain runtime function could not do. - **No runtime cost**: the calls disappear from the output. - **One source of truth**: the options object is generated from the macro, so there is no separate `export default` to keep in sync. The trade-off is that macros look like functions but do not behave like them, which is exactly where the restrictions above surprise people. ## Summary The define* macros are instructions to the SFC compiler, erased before runtime. That is why they need no import, why their arguments cannot see setup-scope variables, and why calling one outside `<script setup>` does nothing but warn.

  • A teammate moves defineProps() into a composable in useCard.ts. What happens?
    The compiler only processes macros inside `<script setup>`, so in a `.ts` file the call reaches the runtime stub exported by `vue`. It declares nothing, and in development it warns that it is a compiler-hint helper whose arguments have no effect. Declare props in the component and pass them to the composable as an argument.
  • Why can defineProps options reference an imported constant but not a ref declared above it?
    The compiler hoists macro arguments into the component definition at module scope. Imports are module-scoped, so they are still in scope there; a `ref` declared in `<script setup>` only exists inside the per-instance `setup()` function, so the compiler raises an error instead of generating code that would reference an undefined variable.
  • What does defineOptions add that previously required a second script block?
    Since 3.3, `defineOptions()` declares component options that have no dedicated macro, such as `name`, `inheritAttrs` or custom plugin options, directly inside `<script setup>`. Before 3.3 you needed a normal `<script>` with `export default { inheritAttrs: false }` beside it.

saying these in an interview costs you the question

  • defineProps must be imported from vue or it is undefined at runtime.
  • Macro arguments can use any variable declared earlier in <script setup>.
  • defineProps works the same way when called inside a composable.
  • defineOptions is the place to declare props and emits together with other options.
  • Aliasing a macro on import, like defineProps as props, is fine.