TypeScript With Vue
Introduction
TypeScript catches prop typos, bad API shapes, and router meta mistakes before runtime. Vue 3 + <script setup lang="ts"> integrates with defineProps, defineEmits, defineModel, and vue-tsc for full-project type checking. This chapter focuses on Vue-specific patterns—not re-teaching TypeScript basics.
Prerequisites
- TypeScript basics
- Components: Props and Emits
- Environment and create-vue Project with TypeScript enabled
Script Setup With Types
<script setup lang="ts">
import { ref, computed } from "vue";
const count = ref(0);
const label = computed(() => `Count is ${count.value}`);
</script>ref(0) infers Ref<number>. Explicit when needed:
const user = ref<User | null>(null);Typed defineProps
Runtime + types (Vue 3.5+ improved):
<script setup lang="ts">
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
</script>Separate declaration (older pattern):
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
});Prefer **interface + generic defineProps<Props>() for new code.
Typed defineEmits
<script setup lang="ts">
const emit = defineEmits<{
save: [payload: { id: number; title: string }];
cancel: [];
}>();
function onSave() {
emit("save", { id: 1, title: "Hello" });
}
</script>Wrong payload shape fails at compile time.
defineModel Types
<script setup lang="ts">
const title = defineModel<string>({ required: true });
const published = defineModel<boolean>("published", { default: false });
</script>Parent:
<PostEditor v-model="postTitle" v-model:published="isPublished" />Component Refs
Template ref to component instance:
Expose methods with defineExpose:
Typing API Responses
src/types/post.ts:
export interface Post {
id: number;
title: string;
slug: string;
published: boolean;
}
export interface PaginatedPosts {
items: Post[];
total: number;
}src/composables/usePosts.ts:
import { useApi } from "./useApi";
import type { Post } from "@/types/post";
export function usePosts() {
return useApi<Post[]>(`${import.meta.env.VITE_API_URL}/api/v1/posts`);
}Narrow unknown JSON at boundaries—do not use any for API data.
Router Typing
src/router/meta.d.ts:
import "vue-router";
declare module "vue-router" {
interface RouteMeta {
requiresAuth?: boolean;
title?: string;
}
}Typed useRoute params:
const route = useRoute();
const slug = computed(() => route.params.slug as string);Stricter: route param typing via definePage in Nuxt; in Vue Router use validation in beforeEnter.
Pinia Store Types
Setup store infers return types automatically:
export const useUserStore = defineStore("user", () => {
const name = ref("");
return { name };
});
// In component:
const userStore = useUserStore();
userStore.name; // typed string ref via storeToRefsOptions store with State interface for complex state.
vue-tsc and Build
create-vue npm run build typically runs:
vue-tsc -b && vite buildvue-tsc type-checks .vue files—Vite alone may not report all TS errors.
CI:
npm run type-check
# or
npx vue-tsc --noEmitCompare TypeScript with Vite—React template differs; jsx not needed for Vue SFC templates.
tsconfig Paths
tsconfig.app.json:
{
"compilerOptions": {
"strict": true,
"moduleResolution": "Bundler",
"paths": {
"@/*": ["./src/*"]
}
}
}Match Vite alias in vite.config.ts.
Common TS Errors in Vue
| Error | Fix |
|---|---|
| Property does not exist on ref | Use .value in script |
| Object is possibly undefined | Narrow with if or ?. |
| No overload matches emit | Fix defineEmits generic |
Cannot find module .vue | Include env.d.ts with *.vue shim |
src/env.d.ts (create-vue includes this):
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, unknown>;
export default component;
}Generic Components (Vue 3.3+)
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
items: T[];
}>();
</script>
<template>
<li v-for="item in items" :key="item.id">{{ item }}</li>
</template>Advanced—use for reusable list components.
Practice Exercise
- Add
Postinterface and typeuseFetch<Post[]> - Type
defineEmitson a form component - Run
vue-tsc --noEmitand fix any errors
FAQ
TypeScript required?
Recommended for this track; JavaScript works with lang="js".
any in templates?
Avoid—type props and store state.
Infer props from runtime defineProps?
Use defineProps<{ ... }>() instead for TS-first projects.
Vetur vs Volar?
Vue - Official (Volar) for Vue 3 + TS.
Strict null checks?
Keep strict: true—matches Hello Code TS track.
Types for Pinia plugins?
See Pinia docs PiniaCustomProperties module augmentation.