In Vue 3's <script setup>, why are defineProps, defineEmits and the other define* macros never imported, and what restrictions follow from that?
answer
- the compiler, not the runtime
- arguments leave the setup scope
- imports yes, locals no
- no aliasing, one call each
basics
~20 sThe 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<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
Recall that the define* macros need no import, work only in <script setup>, and include defineProps, defineEmits, defineModel, defineExpose, defineOptions and defineSlots.
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.
Diagnose macro misuse quickly: calls in composables, aliased imports, options depending on local state, and props smuggled into defineOptions.
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.