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.

Leave a Comment

Your email address will not be published. Required fields are marked *