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 .vue file, 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 a price (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. defineEmits said add sends a name and a price.
  • Hover over dish or emit in 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

  1. How to type props, emits and v-model.
  2. How to type ref, reactive, computed and template refs.
  3. How to share typed values with provide/inject.
  4. How to build a generic component.
  5. How to type a Pinia store and route meta.
  6. What env.d.ts and vue-tsc do.

Props, Emits and v-model

You wantTypeScript way
PropsdefineProps<{ name: string; price?: number }>()
Defaultsconst { price = 50 } = defineProps<…>()
EventsdefineEmits<{ add: [name: string] }>()
v-modelconst qty = defineModel<number>({ default: 1 })
SlotsdefineSlots<{ 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 is null until 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 type unknown.
  • 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">. Then T is "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, d is a Dish and s is a Staff.

Typed Stores and Routes

  • A setup-style Pinia store needs no extra work: its types come from the refs and functions inside.
  • Route meta is untyped by default. Add the fields once in a .d.ts file:
// src/router.d.ts
import "vue-router";

declare module "vue-router" {
  interface RouteMeta {
    requiresAuth?: boolean;
    title?: string;
  }
}
  • Now to.meta.requiresAuth is a boolean | undefined in every guard.

env.d.ts and vue-tsc

  • TypeScript does not understand .vue files by itself. create-vue adds env.d.ts with /// <reference types="vite/client" />, and the editor's Vue extension (Vue - Official) does the rest.
  • import.meta.env.VITE_API_URL gets its type from an ImportMetaEnv interface in that file.
  • vue-tsc checks every .vue and .ts file 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 .vue file.
  • defineProps<T>() = types props. Destructure for defaults.
  • defineEmits<{ e: [args] }>() = types events and their arguments.
  • defineModel<T>() = types a v-model prop.
  • ref<T>() / reactive<T>() = give the type when the start value is empty.
  • useTemplateRef<HTMLInputElement>() = a typed element, null until 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

MistakeWrong ideaCorrect ideaWhat goes wrong
Using any everywhereIt silences errorsUse real types or unknownBugs the checker would catch
ref(null) for an objectTypeScript will guessref<Dish | null>(null)The type is stuck as null
Reading a template ref in setupIt exists right awayUse it in onMounted, with ?.null errors
String keys for provide/injectStrings are simplerUse an InjectionKeyinject() returns unknown
TypeScript inside template expressionsTemplates are TS tooKeep as casts in the scriptErrors in some setups
Relying on vite build for typesThe build checks typesRun vue-tscType 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-model with defineProps, defineEmits and defineModel.
  • Give ref<T>() a type when the start value is empty or null.
  • Use InjectionKey for provide/inject and generic for reusable lists.
  • Run vue-tsc before building. Vite does not check types.

Next: Vue Router Basics