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

Script Setup With Types

vue
<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:

typescript
const user = ref<User | null>(null);

Typed defineProps

Runtime + types (Vue 3.5+ improved):

vue
<script setup lang="ts">
interface Props {
  title: string;
  count?: number;
}
 
const props = withDefaults(defineProps<Props>(), {
  count: 0,
});
</script>

Separate declaration (older pattern):

typescript
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 },
});

Prefer **interface + generic defineProps<Props>() for new code.

Typed defineEmits

vue
<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

vue
<script setup lang="ts">
const title = defineModel<string>({ required: true });
const published = defineModel<boolean>("published", { default: false });
</script>

Parent:

vue
<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:

typescript
export interface Post {
  id: number;
  title: string;
  slug: string;
  published: boolean;
}
 
export interface PaginatedPosts {
  items: Post[];
  total: number;
}

src/composables/usePosts.ts:

typescript
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:

typescript
import "vue-router";
 
declare module "vue-router" {
  interface RouteMeta {
    requiresAuth?: boolean;
    title?: string;
  }
}

Typed useRoute params:

typescript
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:

typescript
export const useUserStore = defineStore("user", () => {
  const name = ref("");
  return { name };
});
 
// In component:
const userStore = useUserStore();
userStore.name; // typed string ref via storeToRefs

Options store with State interface for complex state.

vue-tsc and Build

create-vue npm run build typically runs:

bash
vue-tsc -b && vite build

vue-tsc type-checks .vue files—Vite alone may not report all TS errors.

CI:

bash
npm run type-check
# or
npx vue-tsc --noEmit

Compare TypeScript with Vite—React template differs; jsx not needed for Vue SFC templates.

tsconfig Paths

tsconfig.app.json:

json
{
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "Bundler",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Match Vite alias in vite.config.ts.

Common TS Errors in Vue

ErrorFix
Property does not exist on refUse .value in script
Object is possibly undefinedNarrow with if or ?.
No overload matches emitFix defineEmits generic
Cannot find module .vueInclude env.d.ts with *.vue shim

src/env.d.ts (create-vue includes this):

typescript
/// <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+)

vue
<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

  1. Add Post interface and type useFetch<Post[]>
  2. Type defineEmits on a form component
  3. Run vue-tsc --noEmit and 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.