Vue <script setup>: what the compiler actually does
- Last updated
- Prerequisites:
- Vue SFC basics
- Familiarity with the Options API vs Composition API
- vue
- script-setup
- compiler
- sfc
- ast
- macros
Read at your depth
The practical view
<script setup> lets you write Composition API code with top-level bindings automatically exposed to the template: defineProps, defineEmits, defineExpose, and useSlots/useAttrs are compiler macros (no import needed). Variables, imports, and functions declared at top level are directly usable in <template>. Compared to the Options API, there is no this — props and state are just variables. The compiler also enables defineModel (v-model sugar), and with it the component's public interface is declared via macros rather than an options object.
The same idea in other frameworks
// React: props arrive as a function argument
function Card({ title, count }) {
return <p>{title} · {count}</p>;
}// Angular: @Input() title: string; @Output() tick = new EventEmitter();<script setup lang="ts">
const props = defineProps<{ title: string; count: number }>();
const emit = defineEmits<{ tick: [] }>();
</script>// Svelte 5: export props via $props()
let { title, count }: { title: string; count: number } = $props();Legacy vs modern
Options API boilerplate vs compiled <script setup>
The Options API wires everything through string keys and this; <script setup> compiles top-level bindings directly into a setup function, removing the ceremony.
export default {
props: ['title', 'count'],
emits: ['tick'],
data() { return { open: false }; },
methods: { toggle() { this.open = !this.open; this.$emit('tick'); } }
}<script setup lang="ts">
const props = defineProps<{ title: string; count: number }>();
const emit = defineEmits<{ tick: [open: boolean] }>();
const open = ref(false);
</script>Interview gotchas
Context
This question separates candidates who know the macros are compiler magic from those who assume they are normal imports from 'vue'.
The mechanical answer
They are compiler macros: the SFC compiler detects the calls during compileScript, verifies they are used at the top level of <script setup> (not inside functions or conditionals), and removes them from the output while generating the corresponding runtime props/emits bindings. That is why there is no import — importing would break the static analysis. The top-level constraint exists because the compiler needs a static, unconditional declaration to generate the setup context; a conditional defineProps would make the component contract unknowable at compile time. TypeScript's macro type-checking is provided by the vue language tools injecting ambient declarations.
Trap
The naive answer is 'they are imported from vue'. They are not, and importing defineProps explicitly actually triggers a lint/compiler warning in some setups. The deeper trap: claiming you can call defineProps inside a function to 'conditionally declare props' — the compiler rejects it, because the SFC contract must be static. Interviewers also like when you mention withDefaults as the typed-defaults companion that compiles down to the runtime default assignment.
Context
This probes whether the candidate understands the SFC pipeline (template + script compiled into a render function + component options) rather than just the DX surface.
The mechanical answer
compileScript parses the block with @babel/parser, walks the AST for macros and top-level bindings, then synthesizes a setup() function whose body is your script with the macro calls rewritten (defineProps → __props normalization, defineExpose → setExpose, defineModel → modelRef) and whose return statement exposes the bound names (refs get unref-wrapped, plain values pass through, imports pass through). The template is compiled separately into a render function that closes over the setup scope. The SFC plugin then stitches both into a component object. Nothing about the macros reaches the runtime — the shipped component is ordinary Vue options under the hood, which is why <script setup> components are fully interoperable with Options API components.
Trap
A tempting wrong claim: '<script setup> compiles to the Options API'. The runtime shape is similar (the setup function result feeds the component), but it is the Composition API setup model, not the options object with methods/data. Another trap: 'the compiler runs at runtime in the browser' — the SFC compiler runs at build time; the browser only ever sees plain JavaScript. Mentioning that the setup() body is where 'this' is intentionally absent (props are closure variables, not this.props) is a strong signal.