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:
# 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:
vue add typescript
This command will:
- Install necessary dependencies
- Create a
tsconfig.json
file - Convert some files to TypeScript
- 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:
<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:
<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
<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
<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:
<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:
// 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:
<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:
// 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:
<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
- Start with proper TypeScript configuration: Ensure your
tsconfig.json
is properly set up - Use
defineComponent
helper: Always wrap your component withdefineComponent
for better type inference - Define interfaces for complex data structures: Create interfaces for props, state, and API responses
- Use type annotations when inference fails: Let TypeScript infer types when possible, but add explicit types when needed
- Don't overuse
any
: While tempting, theany
type defeats the purpose of using TypeScript - Use Vue's type utilities: Make use of Vue's built-in type utilities like
PropType
- 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:
<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:
// 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
- Vue.js TypeScript Support Documentation
- TypeScript Handbook
- Vue Class Component
- Pinia - Typed Vue Store
Exercises
- Convert an existing Vue component from JavaScript to TypeScript
- Create a type-safe form with validation using TypeScript
- Implement a custom composable function with proper TypeScript types
- Set up a Vue project with TypeScript, Vue Router, and either Vuex or Pinia
- 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! :)