TypeScript with Vue
By Syncfusion®, Inc. · Last updated
Adding types to Vue code: props, emits and v-model, refs, reactive objects, computed values and template refs, provide/inject with InjectionKey, generic components, typed Pinia stores and route meta, env.d.ts and checking with vue-tsc.
Why Types?
- TypeScript = JavaScript plus types: labels that say what kind of value a variable holds.
- The editor checks types while you type. Mistakes show up before the page runs.
- Vue 3 itself is written in TypeScript, so its types are built in.
- In a
.vuefile, write<script setup lang="ts">to turn TypeScript on. - Types disappear when the code is built. The browser only ever runs JavaScript.
Simple lesson: types catch mistakes early. They change nothing at run time.
Picked JavaScript in the top bar? This lesson is still worth a read, because most job code and libraries use TypeScript. Each box shows the JavaScript version too, so you can compare. If you only write JavaScript, you can skip the exercises' type parts.
A Real-World Example
- Meera's dish card needs a
name(text) and aprice(number). - With types, passing
price="cheap"is a red underline in the editor, not a broken bill later.
- What happened:
defineProps<{ dish: Dish }>()gave the prop a type.defineEmitssaidaddsends a name and a price. - Hover over
dishoremitin the editor to see the types.
In the JS flavour, the same component uses runtime options such as { type: Object, required: true }. Vue checks those in the browser and warns in the Console. TypeScript checks earlier, in the editor.
What You Will Learn
- How to type props, emits and
v-model. - How to type
ref,reactive,computedand template refs. - How to share typed values with
provide/inject. - How to build a generic component.
- How to type a Pinia store and route
meta. - What
env.d.tsandvue-tscdo.
Props, Emits and v-model
| You want | TypeScript way |
|---|---|
| Props | defineProps<{ name: string; price?: number }>() |
| Defaults | const { price = 50 } = defineProps<…>() |
| Events | defineEmits<{ add: [name: string] }>() |
v-model | const qty = defineModel<number>({ default: 1 }) |
| Slots | defineSlots<{ default(props: { item: Dish }): any }>() |
Refs, Reactive, Computed and Template Refs
ref<number>(0)says the ref holds a number. Often TypeScript guesses it from the start value.- Give the type when the start value is empty:
ref<Dish | null>(null),ref<Dish[]>([]). reactive<Order>({...})uses an interface for the whole object.computed<number>(() => …)types the result.useTemplateRef<HTMLInputElement>("box")types an element. It isnulluntil mounted, so use?..
Avoid any. It switches the checks off. Use unknown for data you have not checked yet, such as an API reply, then narrow it.
Typed provide/inject
- A plain string key gives
inject()the typeunknown. - An
InjectionKey<T>is a symbol that carries the type. Provide and inject with the same key. - Keep keys in their own file so both sides import the same symbol.
Generic Components
- A generic component works with any item type, like a list that can show dishes or staff.
- Write
<script setup lang="ts" generic="T">. ThenTis "whatever type the parent passes". - The parent's data decides
T, so the emitted item has the right type too.
- What happened: one component served two lists. In the editor,
dis aDishandsis aStaff.
Typed Stores and Routes
- A setup-style Pinia store needs no extra work: its types come from the refs and functions inside.
- Route
metais untyped by default. Add the fields once in a.d.tsfile:
// src/router.d.ts
import "vue-router";
declare module "vue-router" {
interface RouteMeta {
requiresAuth?: boolean;
title?: string;
}
}
- Now
to.meta.requiresAuthis aboolean | undefinedin every guard.
env.d.ts and vue-tsc
- TypeScript does not understand
.vuefiles by itself.create-vueaddsenv.d.tswith/// <reference types="vite/client" />, and the editor's Vue extension (Vue - Official) does the rest. import.meta.env.VITE_API_URLgets its type from anImportMetaEnvinterface in that file.vue-tscchecks every.vueand.tsfile from the terminal. Vite does not check types while building.- Run it before every build and in CI:
npx vue-tsc --noEmit # check types only
npm run build # create-vue runs: vue-tsc --build && vite build
Vite strips types without checking them, so a type error can still reach production unless vue-tsc runs. Make it part of npm run build or your CI.
Simple lesson: type props, state and keys. Let vue-tsc check everything before you ship.
TL;DR
lang="ts"= turns TypeScript on in a.vuefile.defineProps<T>()= types props. Destructure for defaults.defineEmits<{ e: [args] }>()= types events and their arguments.defineModel<T>()= types av-modelprop.ref<T>()/reactive<T>()= give the type when the start value is empty.useTemplateRef<HTMLInputElement>()= a typed element,nulluntil mounted.InjectionKey<T>= a symbol that carries the type for provide/inject.generic="T"= one component that works with any item type.vue-tsc= checks types from the terminal. Vite does not.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Using any everywhere | It silences errors | Use real types or unknown | Bugs the checker would catch |
ref(null) for an object | TypeScript will guess | ref<Dish | null>(null) | The type is stuck as null |
| Reading a template ref in setup | It exists right away | Use it in onMounted, with ?. | null errors |
| String keys for provide/inject | Strings are simpler | Use an InjectionKey | inject() returns unknown |
| TypeScript inside template expressions | Templates are TS too | Keep as casts in the script | Errors in some setups |
Relying on vite build for types | The build checks types | Run vue-tsc | Type errors reach production |
Check Yourself
Exercises
What to Remember
- Write
<script setup lang="ts">to use TypeScript in a component. - Type props, emits and
v-modelwithdefineProps,defineEmitsanddefineModel. - Give
ref<T>()a type when the start value is empty ornull. - Use
InjectionKeyfor provide/inject andgenericfor reusable lists. - Run
vue-tscbefore building. Vite does not check types.
Next: Vue Router Basics