Vue.js with TypeScript
TypeScript is JavaScript with type annotations. It catches errors before your code runs by checking that values are the correct type. Vue 3 is built with TypeScript and supports it fully. Adding TypeScript to a Vue project gives you better autocomplete, early error detection, and more readable code.
What TypeScript Adds to Vue
Diagram: JavaScript vs TypeScript Error Detection
JavaScript:
function greet(user) {
return "Hello, " + user.name;
}
greet(42); ← No error shown in editor
// Crashes at runtime: Cannot read property 'name' of 42
TypeScript:
function greet(user: { name: string }) {
return "Hello, " + user.name;
}
greet(42); ← Editor shows red underline immediately
// Error: Argument of type 'number' is not assignable
// to parameter of type '{ name: string }'
TypeScript catches the mistake before you run the code.
Setting Up Vue with TypeScript
When creating a new project with Vite, select "Yes" for TypeScript:
npm create vue@latest ? Add TypeScript? → Yes
For an existing project, add TypeScript support:
npm install -D typescript vue-tsc
Change your script blocks to use lang="ts":
<script setup lang="ts"> // TypeScript code here </script>
Type Annotations in script setup
Typing ref()
TypeScript usually infers the type from the initial value. Add an explicit type with angle brackets when the type cannot be inferred.
<script setup lang="ts">
import { ref } from "vue";
// Type inferred from initial value
const count = ref(0); // Ref<number>
const name = ref("Alice"); // Ref<string>
const active = ref(true); // Ref<boolean>
// Explicit type — starts as null but will hold a User later
const user = ref<User | null>(null);
// Array of a specific type
const items = ref<string[]>([]);
</script>
Typing reactive()
<script setup lang="ts">
import { reactive } from "vue";
interface FormData {
name: string;
email: string;
age: number;
}
const form = reactive<FormData>({
name: "",
email: "",
age: 0
});
</script>
Defining Interfaces and Types
Define the shape of your data objects using interface or type. This documents your data structure and enables autocomplete.
<script setup lang="ts">
import { ref } from "vue";
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
tags?: string[]; // optional field (the ? means it can be absent)
}
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer"; // union type (only these 3 values)
}
const products = ref<Product[]>([]);
const currentUser = ref<User | null>(null);
</script>
Diagram: Interface as a Contract
interface Product {
id: number; ← must be a number
name: string; ← must be a string
price: number; ← must be a number
inStock: boolean; ← must be true or false
}
Valid:
{ id: 1, name: "Chair", price: 99, inStock: true } ✓
Invalid:
{ id: "one", name: "Chair" }
← id is a string (should be number) ✗
← price is missing ✗
TypeScript reports these errors before the app runs.
Typing Props with defineProps
In <script setup lang="ts">, use TypeScript's type syntax directly with defineProps.
<script setup lang="ts">
interface Props {
title: string;
count: number;
active?: boolean; // optional
}
const props = defineProps<Props>();
// Access props:
console.log(props.title); // string — TypeScript knows the type
console.log(props.count); // number
</script>
<template>
<div>
<h3>{{ props.title }}</h3>
<p>Count: {{ props.count }}</p>
</div>
</template>
Props with Defaults using withDefaults
<script setup lang="ts">
interface Props {
title: string;
size?: "small" | "medium" | "large";
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
size: "medium",
disabled: false
});
</script>
Typing Emits with defineEmits
<script setup lang="ts">
const emit = defineEmits<{
(e: "submit", payload: { name: string; email: string }): void;
(e: "cancel"): void;
(e: "update:modelValue", value: string): void;
}>();
function handleSubmit() {
emit("submit", { name: "Alice", email: "alice@example.com" });
}
</script>
TypeScript will report an error if you call emit with the wrong event name or incorrect payload type.
Typing Computed Properties
<script setup lang="ts">
import { ref, computed } from "vue";
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
const cartItems = ref<CartItem[]>([]);
// TypeScript infers the return type as number
const cartTotal = computed(() =>
cartItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// Explicit return type annotation
const formattedTotal = computed((): string =>
"$" + cartTotal.value.toFixed(2)
);
</script>
Typing Template Refs
Template refs (accessing DOM elements directly) need an explicit type to let TypeScript know what kind of element you are accessing.
<script setup lang="ts">
import { ref, onMounted } from "vue";
const inputEl = ref<HTMLInputElement | null>(null);
const divEl = ref<HTMLDivElement | null>(null);
onMounted(() => {
inputEl.value?.focus(); // TypeScript knows .focus() exists on input
console.log(divEl.value?.clientWidth); // TypeScript knows .clientWidth exists on div
});
</script>
<template>
<input ref="inputEl" type="text">
<div ref="divEl">Content</div>
</template>
Typing API Responses
Define interfaces that match your API's response shape and type the data ref accordingly.
<script setup lang="ts">
import { ref, onMounted } from "vue";
interface ApiUser {
id: number;
name: string;
email: string;
username: string;
}
const users = ref<ApiUser[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
onMounted(async () => {
loading.value = true;
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data: ApiUser[] = await res.json();
users.value = data;
} catch (err) {
error.value = (err as Error).message;
} finally {
loading.value = false;
}
});
</script>
Diagram: TypeScript Benefits in a Vue Component
Without TypeScript: const user = ref(null); user.value.name ← No error shown, but crashes if user is null user.value.nmae ← Typo not caught — silently undefined With TypeScript: const user = ref<User | null>(null); user.value.name ← Editor warns: user could be null user.value?.name ← Correct: optional chaining, no crash user.value.nmae ← Editor warns: 'nmae' does not exist on User
Options API with TypeScript
If you prefer the Options API, use defineComponent to enable full TypeScript support.
<script lang="ts">
import { defineComponent, ref } from "vue";
interface Product {
id: number;
name: string;
price: number;
}
export default defineComponent({
name: "ProductList",
props: {
category: {
type: String,
required: true
}
},
data() {
return {
products: [] as Product[]
};
},
computed: {
productCount(): number {
return this.products.length;
}
}
});
</script>
Summary
TypeScript integrates into Vue 3 through lang="ts" on script blocks. Type your ref() and reactive() values with generics and define data shapes with interfaces. Use defineProps<Props>() and defineEmits<{...}>() for fully typed component APIs. Type template refs with the specific HTML element type. TypeScript's main value in Vue is catching type mismatches, missing properties, and incorrect function arguments at edit time — before you ever run the code — which makes Vue applications easier to maintain as they grow.
