skip to content

Type Safety

Typing Vue code end to end: type-based props and emits, typed refs and injection keys, generic components, and SFC checking with vue-tsc. Interviewers probe how the macros infer types.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

20

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.
open as a page

In a Vue 3.5 `<script setup lang="ts">` component, how do you declare props with a TypeScript type, and what does the compiler generate from it?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Pass a type argument to defineProps, for example defineProps<{ options: Option[]; selected?: string }>(). The SFC compiler reads that type and generates the runtime props declaration: non-optional props become required, and constructors such as Array and String are inferred.

open as a page

In Vue 3 with TypeScript, how do you type a ref that may hold `string | number`, and what does `ref<number>()` without an argument produce?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Pass a type argument, ref<string | number>('2020'), or annotate the variable as Ref<string | number>. Without either, the type is inferred from the initial value only. ref<number>() with no argument is typed Ref<number | undefined>.

open as a page

In a Vite-based Vue 3 TypeScript project, why can the dev server and production build succeed despite type errors, and what catches them?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Vue's Vite-based setup is transpilation-only: the dev server and bundler strip types from each file without type-checking, to stay fast. Type errors are caught by the editor's Vue language tools and by running vue-tsc --noEmit in a script or CI.

open as a page

In Vue 3.5, how do `withDefaults` and destructured defaults differ for type-based props such as a SelectInput's `options?: Option[]`?

level: middleimportance: must knowfreq 55%

basics

~20 s

withDefaults(defineProps<Props>(), { options: () => [] }) needs factory functions for mutable defaults. Since 3.5, destructuring, const { options = [] } = defineProps<Props>(), lets the compiler add the factory. Both compile to runtime defaults.

open as a page

In a Vue 3.5 `<script setup lang="ts">` component, how do you type template refs to an `<input>` element and to a child component?

level: middleimportance: must knowfreq 55%

basics

~10 s

Use useTemplateRef: the Vue 3.5 language tools infer static refs, or you write useTemplateRef<HTMLInputElement>('input'). For a component, use InstanceType<typeof Child>. The value is null until mount, so access it with optional chaining.

open as a page

In a Vue `<script setup lang="ts">` component, how do you type a native `@change` handler so reading the input's value compiles?

level: juniorimportance: should knowfreq 40%

basics

~10 s

Annotate the parameter, function onChange(event: Event), then narrow the target: (event.target as HTMLInputElement).value, or an instanceof check. Without the annotation the parameter is implicitly any, which strict TypeScript rejects.

open as a page

Which editor tooling does Vue 3 officially provide for TypeScript in single-file components, and how does it relate to vue-tsc?

level: juniorimportance: should knowfreq 40%

basics

~20 s

The official Vue - Official extension (previously Volar) bundles the Vue language server and a TypeScript plugin that lets the editor's TypeScript service understand .vue files. It shares its core with vue-tsc, so editor and CLI checks normally agree.

open as a page

In a Vue 3.5 `<script setup lang="ts">` component, how do you declare a generic item type `T`, and how is it chosen at each usage?

level: middleimportance: should knowfreq 45%

basics

~20 s

Add a generic attribute to the script tag, for example generic="T extends { id: string }", and use T in defineProps and defineEmits. Each parent usage infers T from the props it passes; the parameter exists only for type checking.

open as a page

In a Vue 3.3+ `<script setup lang="ts">` component, what are the two type syntaxes for defineEmits, and what reaches the runtime?

level: middleimportance: should knowfreq 50%

basics

~20 s

Either call signatures, { (e: 'change', value: string): void }, or since 3.3 named tuples, { change: [value: string] }. Both type emit() calls and parent handlers the same way; at runtime the compiler emits only the list of event names.

open as a page

In Vue 3, how do you type a cart service shared through provide/inject so the provider and every consumer agree on its type?

level: middleimportance: should knowfreq 45%

basics

~10 s

Export a typed key, const CartKey: InjectionKey<CartService> = Symbol('cart'), from one module. provide(CartKey, value) is checked against CartService, and inject(CartKey) returns CartService | undefined, which a default or a throwing useCart() helper narrows.

open as a page

In Vue 3, why does the documentation recommend `const book: Book = reactive({...})` over passing a type argument as in `reactive<Book>({...})`?

level: middleimportance: should knowfreq 30%

basics

~20 s

reactive() returns an unwrapped type, Reactive<T>, which differs from the type argument whenever the object contains refs. Annotating the variable with an interface describes the shape you actually read and write, so the docs advise annotation instead of reactive<T>().

open as a page

What is vue-tsc, and why can't plain tsc type-check Vue single-file components and their templates?

level: middleimportance: should knowfreq 45%

basics

~20 s

Plain tsc only understands TypeScript and JavaScript files, not .vue. vue-tsc wraps tsc with Vue's language plugin, which turns each SFC, template expressions included, into virtual TypeScript that tsc checks, and it accepts tsc's usual flags.

open as a page

In a generic Vue `<script setup generic="T">` List component, how do you declare a typed `item` slot so the parent's slot props are checked as `T`?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Call defineSlots with a type literal such as item(props: { item: T; index: number }): any. Vue's language tools then check the child's slot element against it and type the parent's #item props as the inferred T; runtime behaviour is unchanged.

open as a page

A Vue 3.5 component declares `size?: 'sm' | 'md' | 'lg'` and `disabled?: boolean` with type-based defineProps; what does Vue actually check and change at runtime?

level: seniorimportance: should knowfreq 35%

basics

~20 s

At runtime size is only checked as a String, and only in development, so 'xl' passes silently; the literal union is enforced only by vue-tsc. disabled gets Boolean casting: absent means false, and its type is boolean, not optional.

open as a page

A Vue 3.5 build fails with "Unresolvable type reference or unsupported built-in utility type" on `defineProps<Props>()`; what causes it, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The SFC compiler builds runtime props by reading the type's syntax, not by running TypeScript, so only interfaces, type literals and a few utility types resolve. Rewrite the props type into such a shape or use a runtime declaration.

open as a page

In a Vue 3 project, the CI type-check step passes but the editor shows type errors in a component's template. What would you investigate?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Check whether CI really runs vue-tsc (not tsc or just the build), whether its tsconfig includes the .vue file, whether a references-only root config checks nothing, and whether TypeScript, tool versions or vueCompilerOptions differ from the editor's.

open as a page

In Vue's language tooling, what does the vueCompilerOptions setting strictTemplates change, and what is checked in templates without it?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

strictTemplates is a vueCompilerOptions flag in tsconfig.json, off by default. Turning it on enables strictVModel and the checkUnknownProps, Events, Components and Directives checks, so unknown names in templates become errors in vue-tsc and the editor.

open as a page

A generic Vue `<script setup generic="T">` List exposes `scrollTo(item: T)` with defineExpose; why does `InstanceType<typeof List>` fail for the parent's ref, and what works instead?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

Vue's language tools type a generic SFC as a generic function, not a constructor, so InstanceType has nothing to extract. Use ComponentExposed<typeof List> from vue-component-type-helpers, or let language-tools 2.1+ infer a static template ref.

open as a page

In a generic Vue composable `function useSelection<T>(initial: T)`, why is `ref(initial)` not assignable to `Ref<T>`, and how do you fix it?

level: seniorimportance: nice to knowfreq 20%

basics

~10 s

ref(initial) is typed Ref<UnwrapRef<T>>, because ref unwraps nested refs, and for an unresolved T TypeScript cannot prove UnwrapRef<T> equals T. Cast with as Ref<T>, or use shallowRef(initial), which is typed ShallowRef<T> exactly.

open as a page