Skip to main content

Vue.js TypeScript Support

Introduction

TypeScript has become increasingly popular in the JavaScript ecosystem due to its ability to add static type checking to your code. Vue.js offers excellent TypeScript support, allowing developers to create more robust applications with fewer runtime errors. This guide will walk you through integrating TypeScript with Vue.js, from basic setup to advanced features.

TypeScript in Vue.js provides several benefits:

  • Type safety: Catch errors during development rather than at runtime
  • Better IDE support: Enhanced autocompletion, navigation, and refactoring
  • Improved documentation: Types serve as documentation for your code
  • Maintainability: Easier to maintain and refactor large codebases

Getting Started with TypeScript in Vue.js

Setting Up a New TypeScript Vue Project

The easiest way to start a TypeScript Vue project is by using Vue CLI:

bash
# Install Vue CLI if you haven't already
npm install -g @vue/cli

# Create a new project with TypeScript
vue create my-ts-app

# When prompted, manually select features
# Make sure to select TypeScript

When setting up your project, you'll encounter several TypeScript-related options:

  • Class-style component syntax
  • Babel alongside TypeScript
  • TSLint or ESLint integration

For beginners, selecting TypeScript with ESLint is a good starting point.

Adding TypeScript to an Existing Vue Project

If you already have a Vue.js project, you can add TypeScript support:

bash
vue add typescript

This command will:

  1. Install necessary dependencies
  2. Create a tsconfig.json file
  3. Convert some files to TypeScript
  4. Update your build configuration

Basic TypeScript Usage in Vue Components

Component Structure

Vue offers multiple ways to define components with TypeScript. Let's start with the Options API approach:

ts
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
type: String,
required: true
}
},
data() {
return {
count: 0,
name: 'Vue + TypeScript'
}
},
methods: {
increment(): void {
this.count++
}
}
})
</script>

<template>
<div>
<h1>{{ msg }}</h1>
<p>{{ name }}</p>
<button @click="increment">Count: {{ count }}</button>
</div>
</template>

Note the important <script lang="ts"> attribute, which tells Vue to process the script as TypeScript.

Type-Safe Props

TypeScript enhances Vue's prop validation by adding compile-time type checking. Here's how to define type-safe props:

ts
<script lang="ts">
import { defineComponent, PropType } from 'vue'

interface User {
id: number
name: string
email: string
}

export default defineComponent({
props: {
user: {
type: Object as PropType<User>,
required: true
},
users: {
type: Array as PropType<User[]>,
default: () => []
},
status: {
type: String as PropType<'active' | 'inactive' | 'pending'>,
default: 'active'
}
}
})
</script>

The PropType utility helps TypeScript understand complex prop types beyond the basic JavaScript types.

Composition API with TypeScript

Vue 3's Composition API works exceptionally well with TypeScript, providing excellent type inference.

Basic Composition API Example

ts
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
setup() {
const count = ref<number>(0)
const doubleCount = computed(() => count.value * 2)

function increment(): void {
count.value++
}

return {
count,
doubleCount,
increment
}
}
})
</script>

TypeScript automatically infers most types, but you can explicitly define them when needed.

Typed Event Handlers

ts
<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup() {
const inputValue = ref('')

// Explicitly typed event handler
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
inputValue.value = target.value
}

return {
inputValue,
handleInput
}
}
})
</script>

<template>
<input :value="inputValue" @input="handleInput" />
<p>Current value: {{ inputValue }}</p>
</template>

Script Setup Syntax with TypeScript

Vue 3's <script setup> syntax offers a more concise way to use the Composition API with TypeScript:

ts
<script setup lang="ts">
import { ref, computed } from 'vue'

// Props can be defined with defineProps
const props = defineProps<{
title: string
likes?: number
}>()

// Emits can be typed with defineEmits
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', id: number): void
}>()

const count = ref(0)
const doubleCount = computed(() => count.value * 2)

function increment() {
count.value++
if (props.likes && count.value > props.likes) {
emit('update', 1)
}
}
</script>

This approach offers excellent type inference with minimal boilerplate code.

Working with the Vue Router in TypeScript

When using Vue Router with TypeScript, you can define properly typed route parameters:

ts
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import UserProfile from '../views/UserProfile.vue'

const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/user/:id',
name: 'UserProfile',
component: UserProfile,
props: true
}
]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router

In a component, you can use typed route parameters:

ts
<script setup lang="ts">
import { useRoute } from 'vue-router'

// Route params are properly typed
const route = useRoute()
const userId = computed(() => Number(route.params.id))
</script>

Using Vuex with TypeScript

Vuex, Vue's state management library, can be strongly typed with TypeScript:

ts
// store/index.ts
import { createStore } from 'vuex'

interface User {
id: number
name: string
}

interface RootState {
users: User[]
loading: boolean
}

export default createStore<RootState>({
state: {
users: [],
loading: false
},
getters: {
getUser: (state) => (id: number) => {
return state.users.find(user => user.id === id)
}
},
mutations: {
SET_USERS(state, users: User[]) {
state.users = users
},
SET_LOADING(state, loading: boolean) {
state.loading = loading
}
},
actions: {
async fetchUsers({ commit }) {
commit('SET_LOADING', true)
try {
const response = await fetch('/api/users')
const users: User[] = await response.json()
commit('SET_USERS', users)
} finally {
commit('SET_LOADING', false)
}
}
}
})

Real-World Example: Todo Application

Let's see how the concepts come together in a simple todo app:

ts
<script setup lang="ts">
import { ref, computed } from 'vue'

interface Todo {
id: number
text: string
completed: boolean
}

const todos = ref<Todo[]>([])
const newTodo = ref('')

const completedTodos = computed(() => {
return todos.value.filter(todo => todo.completed)
})

const incompleteTodos = computed(() => {
return todos.value.filter(todo => !todo.completed)
})

function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
}

function toggleTodo(todo: Todo) {
todo.completed = !todo.completed
}

function removeTodo(id: number) {
todos.value = todos.value.filter(todo => todo.id !== id)
}
</script>

<template>
<div>
<h1>TypeScript Todo App</h1>

<form @submit.prevent="addTodo">
<input v-model="newTodo" placeholder="Add a new todo" />
<button type="submit">Add</button>
</form>

<h2>Todo List</h2>
<ul v-if="incompleteTodos.length">
<li v-for="todo in incompleteTodos" :key="todo.id">
<label>
<input type="checkbox" @change="toggleTodo(todo)" :checked="todo.completed" />
{{ todo.text }}
</label>
<button @click="removeTodo(todo.id)">Delete</button>
</li>
</ul>
<p v-else>No incomplete todos!</p>

<h2>Completed ({{ completedTodos.length }})</h2>
<ul v-if="completedTodos.length">
<li v-for="todo in completedTodos" :key="todo.id">
<label>
<input type="checkbox" @change="toggleTodo(todo)" :checked="todo.completed" />
<s>{{ todo.text }}</s>
</label>
<button @click="removeTodo(todo.id)">Delete</button>
</li>
</ul>
<p v-else>No completed todos yet!</p>
</div>
</template>

Best Practices for TypeScript in Vue.js

  1. Start with proper TypeScript configuration: Ensure your tsconfig.json is properly set up
  2. Use defineComponent helper: Always wrap your component with defineComponent for better type inference
  3. Define interfaces for complex data structures: Create interfaces for props, state, and API responses
  4. Use type annotations when inference fails: Let TypeScript infer types when possible, but add explicit types when needed
  5. Don't overuse any: While tempting, the any type defeats the purpose of using TypeScript
  6. Use Vue's type utilities: Make use of Vue's built-in type utilities like PropType
  7. Consider class-based components: For complex components, class-based components using vue-class-component might be cleaner

Common TypeScript Patterns in Vue

Type Assertions for Refs and Templates

When working with template refs, proper typing is important:

ts
<script setup lang="ts">
import { ref, onMounted } from 'vue'

// Define the element type
const fileInput = ref<HTMLInputElement | null>(null)

onMounted(() => {
// Now TypeScript knows fileInput.value has HTMLInputElement methods
if (fileInput.value) {
fileInput.value.accept = 'image/*'
}
})

// Function accepting an HTMLElement
function focusInput() {
fileInput.value?.focus()
}
</script>

<template>
<input ref="fileInput" type="file" />
<button @click="focusInput">Focus input</button>
</template>

Custom Type Declarations

Sometimes you'll need to create custom type declarations for libraries or global variables:

ts
// shims-vue.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

// Add types for a plugin
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$toast: {
show(message: string, options?: { duration?: number }): void
error(message: string): void
success(message: string): void
}
}
}

Summary

Vue.js offers excellent TypeScript support that can help you build more robust applications. We've covered:

  • Setting up TypeScript in new and existing Vue projects
  • Writing components with TypeScript using the Options API
  • Leveraging the Composition API with TypeScript
  • Using the concise script setup syntax
  • Working with Vue Router and Vuex in a type-safe manner
  • Building a real-world example with TypeScript
  • Best practices and common patterns

TypeScript adds an additional learning curve, but the benefits in terms of code quality, maintainability, and developer experience are substantial, especially for larger projects.

Additional Resources

Exercises

  1. Convert an existing Vue component from JavaScript to TypeScript
  2. Create a type-safe form with validation using TypeScript
  3. Implement a custom composable function with proper TypeScript types
  4. Set up a Vue project with TypeScript, Vue Router, and either Vuex or Pinia
  5. Create a small application that fetches and displays data from an API, using proper TypeScript interfaces for the API responses


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)