skip to content

In Vue 3 with TypeScript, why do you wrap a component in defineComponent() when you are not using `<script setup>`?

level: juniorimportance: must knowfreq 55%

answer

  1. a type helper, not a factory
  2. gives this its component type
  3. infers props passed to setup()
  4. a function form since 3.3
  5. script setup gets it automatically

basics

~20 s

defineComponent is a type helper: it returns the options object unchanged at runtime but lets TypeScript infer props, data and computed types on this and in setup(props). Its 3.3+ function form also supports generic render-function components.

solid answer

~50 s

A plain object literal gives TypeScript no idea that it is a Vue component, so `this` in `mounted` or `props` in `setup` is not the component's type. `defineComponent()` is Vue's **type helper** for that: with `props: { msg: { type: String, required: true } }`, `this.msg` becomes `string`, an optional `name: String` becomes `string | undefined`, and `data`, `computed` and methods land on `this` too. At runtime it returns the options object unchanged. Since 3.3 it also accepts a setup-style function, `defineComponent(<T extends string | number>(props: { msg: T }) => () => h('div', props.msg), { props: ['msg'] })`, which is how render-function or TSX components become generic; the runtime props list must still be declared by hand. A TypeScript `<script setup>` never needs it written: the SFC compiler wraps its output in `defineComponent` itself.

go deeper

for a junior

Recall that defineComponent exists for TypeScript inference of props and this, and that script setup components do not need it.

for a middle

Explain how props options map to types (required, String to string) and that at runtime the object form returns the options unchanged.

for a senior

Know the 3.3 function signature for generic render-function components and why its runtime props list must still be written by hand.

for a principal

Choose between script setup generics and defineComponent's function form for a library, weighing template checking against TSX flexibility and duplicated props lists.

## The problem it solves To TypeScript, `export default { props: { msg: String }, mounted() { this.msg } }` is just an object literal with some methods. Nothing tells the checker that `this` inside `mounted` should be a Vue component instance with a `msg` prop, a `data` state and computed properties. **`defineComponent()`** is Vue's answer: a **type helper** whose overloads describe the whole component-options contract, so TypeScript can infer the instance type from the options you pass. ```ts import { defineComponent } from 'vue' export default defineComponent({ props: { name: String, msg: { type: String, required: true } }, data() { return { count: 1 } }, mounted() { this.name // string | undefined this.msg // string this.count // number } }) ``` ## What it infers - **Props** from the runtime `props` option: `required: true` removes `undefined`; the `type` constructor maps to the TypeScript type (`String` to `string`); `PropType<Book>` casts supply complex types. - **`data`** return values, **`computed`** getters and **methods**, all merged onto `this`. - **`setup(props)`**: in Composition API code without `<script setup>`, `props` is typed from the same `props` option. - **The component's own type**, which parents and tooling use to check how it is used. The docs add that `defineComponent()` also enables inference for components written in **plain JavaScript**, because editors use the same type information. ## What it does at runtime | Call form | Runtime result | |---|---| | `defineComponent({ ... })` | The **same options object**, unchanged | | `defineComponent(setupFn, extraOptions?)` | A new options object: the function's `name`, the extra options, and the function as `setup` | So the object form costs nothing, and the function form is a thin wrapper. It is not a factory or a registration step; it does not register the component anywhere. ## The function signature and generic components Vue 3.3 added a second signature aimed at Composition API code with **render functions or TSX**. You pass a function that works like `setup()`: it receives props and the setup context and returns a render function. Because it is an ordinary TypeScript function, it can take **type parameters**: ```tsx import { defineComponent, ref } from 'vue' const Comp = defineComponent( <T extends string | number>(props: { msg: T; list: T[] }) => { const count = ref(0) return () => <div>{count.value} {props.msg}</div> }, // manual runtime props declaration is still needed { props: ['msg', 'list'] } ) ``` Two things to remember about this form: 1. It is one of the **two supported routes** to generic components, alongside `<script setup>` with the `generic` attribute. 2. The type parameters describe types only, so the **runtime props list must be declared by hand** in the second argument; the docs note an automatic inference plugin is only planned. ## Typing props that `String` cannot describe Runtime prop constructors only reach so far: `Object` or `Array` alone tells TypeScript nothing about the shape inside. The Options API answer is a cast with the `PropType` utility type, still inside `defineComponent`: ```ts import { defineComponent, type PropType } from 'vue' interface Book { title: string; year: number } export default defineComponent({ props: { book: { type: Object as PropType<Book>, required: true } }, computed: { label(): string { return `${this.book.title} (${this.book.year})` } } }) ``` The runtime check is still just `Object`; the cast only informs the types. Note also the explicit `: string` return annotation on the computed, which helps TypeScript when inference through `this` becomes circular. ## When you do not write it In a TypeScript `<script setup>` block you never call `defineComponent` yourself: the SFC compiler generates `export default defineComponent({ setup(...) { ... } })` around your code, so the exported type is still a proper component. You write it in: - `.ts` or `.tsx` files that define components with render functions; - Options API components in `<script lang="ts">`; - Composition API components that use `setup()` directly. ## Common mistakes - Believing it **registers** the component globally; registration is `app.component()` or a local import. - Believing it is required for **runtime** behaviour; the object form is an identity function. - Forgetting the **runtime props** in the function form, so props arrive as fallthrough attributes instead. - Wrapping a `<script setup>` component in it manually.

  • What goes wrong if you omit the runtime props list in Vue's defineComponent function signature?
    The type parameters exist only for TypeScript, so Vue has no runtime record of which attributes are props. The values then arrive as fallthrough attributes in `attrs` instead of on `props`, even though the types claim otherwise. The docs say the manual `props` declaration in the second argument is still needed.
  • Do you need defineComponent in a TypeScript `<script setup>` single-file component?
    No. For TypeScript blocks the SFC compiler wraps the generated `setup` in `defineComponent` itself, so the exported component already carries full types. You write it only for Options API code, a plain `setup()` component or a render-function component outside script setup.

saying these in an interview costs you the question

  • defineComponent registers the component globally so templates can find it.
  • Without defineComponent the component would not work at runtime.
  • defineComponent returns a component class, like Vue.extend did in Vue 2.
  • The function form infers runtime props from the type parameters automatically.
  • Script setup components must also be wrapped in defineComponent by hand.