Skip to the solution
MediumFrontend9 min read

What do defineOptions() and defineSlots() do in <script setup>?

1.1k views

The problem

Question presented to candidate: "In a <script setup> component I need to set inheritAttrs: false, give the component a name, and document which slots it accepts and what each slot receives. How do defineOptions() and defineSlots() help, and what are their limits?"

What a strong answer should cover:

  • Both are compiler macros (Vue 3.3+): no import, compiled away, usable only in <script setup>.
  • defineOptions({...}) declares component options that have no macro of their own, mainly name and inheritAttrs; it is hoisted out of setup().
  • It rejects props, emits, expose and slots (use the dedicated macros), rejects type arguments, and cannot reference local variables other than literal constants.
  • defineSlots<{...}>() is type-only: it describes each slot and its props for the IDE and vue-tsc, and returns the same object as useSlots().
  • defineSlots checks both sides: the props the component passes to <slot> and the destructured slot props in the parent.
  • Before 3.3 the same needed a second plain <script> block for options, or no slot typing at all.

Clarifying questions expected:

  • "Which Vue version is the project on? Both macros need 3.3 or later."
  • "Is the goal type safety in the editor, or runtime behaviour? defineSlots only affects types."

Code / implementation expected: Optional β€” a component using both macros, ideally with a slot prop type error shown by vue-tsc.

  • defineOptions
  • defineSlots
  • macros

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 11 sections

Read the code

VuedefineOptions names a recursive tree and turns off attribute fallthrough; defineSlots types the label slot
<script setup lang="ts">
import { useAttrs } from "vue";

type Node = { label: string; children?: Node[] };

// defineOptions: component options that have no macro of their own.
defineOptions({
  name: "TreeNode",     // lets the template refer to itself as <TreeNode>
  inheritAttrs: false,  // attrs go where we bind them, not on the root element
});

// defineSlots: type-only declaration of the slots and the props each slot receives.
const slots = defineSlots<{
  label?(props: { node: Node; depth: number }): any;
}>();

const props = withDefaults(defineProps<{ node?: Node; depth?: number }>(), {
  node: () => ({
    label: "src",
    children: [
      { label: "components", children: [{ label: "TreeNode.vue" }] },
      { label: "main.ts" },
    ],
  }),
  depth: 0,
});

const attrs = useAttrs();
if (props.depth === 0) console.log("has a label slot?", !!slots.label);
</script>

<template>
  <li class="row">
    <span v-bind="attrs" :style="{ paddingLeft: depth * 16 + 'px' }">
      <slot name="label" :node="node" :depth="depth">{{ node.label }}</slot>
    </span>
    <ul v-if="node.children">
      <TreeNode
        v-for="child in node.children"
        :key="child.label"
        :node="child"
        :depth="depth + 1"
        data-kind="child"
      />
    </ul>
  </li>
</template>
VuedefineSlots catches a wrong slot prop in the parent at type-check time (vue-tsc)
<!-- FileList.vue: declares its slots and their props with defineSlots -->
<script setup lang="ts">
export type FileEntry = { name: string; size: number };
defineProps<{ files: FileEntry[] }>();
defineSlots<{
  default?(props: { file: FileEntry; index: number }): any;
  empty?(): any;
}>();
</script>
<template>
  <ul v-if="files.length">
    <li v-for="(file, index) in files" :key="file.name"><slot :file="file" :index="index">{{ file.name }}</slot></li>
  </ul>
  <slot v-else name="empty" />
</template>

<!-- Parent.vue: vue-tsc reports error TS2551 on file.sizeKb (Did you mean 'size'?) -->
<script setup lang="ts">
import FileList from "./FileList.vue";
const files = [{ name: "a.ts", size: 120 }];
</script>
<template>
  <FileList :files="files">
    <template #default="{ file, index }">
      {{ index }}: {{ file.name }} ({{ file.sizeKb }} KB)
    </template>
    <template #footer>never declared</template>
  </FileList>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 25 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

How does <Suspense> work in Vue 3.5 after the 2024 stabilization?

Continue