TypeScript Best Practices for Vue Applications

Essential TypeScript patterns and practices for building type-safe Vue applications

TypeScript Best Practices for Vue Applications

TypeScript has revolutionized how we write JavaScript by adding static type checking. When combined with Vue.js, it provides an excellent developer experience with better IDE support, early error detection, and improved code maintainability.

Setting Up TypeScript in Vue

Basic Configuration

Your tsconfig.json should include these essential settings:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true
  }
}

Type-Safe Component Development

Defining Props with TypeScript

Use interfaces for complex prop definitions:

// types/blog.ts
export interface BlogPost {
  id: string
  title: string
  content: string
  author: Author
  publishedAt: Date
  tags: string[]
  metadata?: PostMetadata
}

export interface Author {
  name: string
  email: string
  avatar?: string
}

export interface PostMetadata {
  readingTime: number
  wordCount: number
  featured: boolean
}
<!-- components/BlogPost.vue -->
<template>
  <article class="blog-post">
    <header>
      <h1>{{ post.title }}</h1>
      <div class="author-info">
        <img :src="post.author.avatar" :alt="post.author.name">
        <span>{{ post.author.name }}</span>
      </div>
    </header>
    <div v-html="post.content"></div>
  </article>
</template>

<script setup lang="ts">
import type { BlogPost } from '~/types/blog'

interface Props {
  post: BlogPost
  showMetadata?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showMetadata: true
})
</script>

Composables with TypeScript

Create type-safe composables:

// composables/useBlog.ts
export const useBlog = () => {
  const posts = ref<BlogPost[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  const fetchPosts = async (): Promise<void> => {
    loading.value = true
    error.value = null
    
    try {
      const response = await $fetch<{ posts: BlogPost[] }>('/api/posts')
      posts.value = response.posts
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'An error occurred'
    } finally {
      loading.value = false
    }
  }

  const getPostById = (id: string): BlogPost | undefined => {
    return posts.value.find(post => post.id === id)
  }

  const getPostsByAuthor = (authorName: string): BlogPost[] => {
    return posts.value.filter(post => post.author.name === authorName)
  }

  return {
    posts: readonly(posts),
    loading: readonly(loading),
    error: readonly(error),
    fetchPosts,
    getPostById,
    getPostsByAuthor
  }
}

Advanced TypeScript Patterns

Generic Components

Create reusable components with generics:

<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <th v-for="column in columns" :key="column.key">
            {{ column.label }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in items" :key="getItemKey(item)">
          <td v-for="column in columns" :key="column.key">
            <component 
              :is="column.component || 'span'"
              :item="item"
              :value="getValue(item, column.key)"
            />
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts" generic="T extends Record<string, any>">
interface Column<T> {
  key: keyof T
  label: string
  component?: Component
}

interface Props<T> {
  items: T[]
  columns: Column<T>[]
  keyField: keyof T
}

const props = defineProps<Props<T>>()

const getItemKey = (item: T): string => {
  return String(item[props.keyField])
}

const getValue = (item: T, key: keyof T): any => {
  return item[key]
}
</script>

Type Guards

Implement type guards for runtime type checking:

// utils/typeGuards.ts
export function isString(value: unknown): value is string {
  return typeof value === 'string'
}

export function isNumber(value: unknown): value is number {
  return typeof value === 'number' && !isNaN(value)
}

export function isBlogPost(value: unknown): value is BlogPost {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'title' in value &&
    'content' in value &&
    'author' in value &&
    isString((value as any).id) &&
    isString((value as any).title)
  )
}

// Usage in components
const validateAndSetPost = (data: unknown) => {
  if (isBlogPost(data)) {
    post.value = data // TypeScript knows this is BlogPost
  } else {
    throw new Error('Invalid blog post data')
  }
}

Utility Types

Leverage TypeScript utility types:

// Create partial update types
type BlogPostUpdate = Partial<Pick<BlogPost, 'title' | 'content' | 'tags'>>

// Create required subsets
type BlogPostSummary = Pick<BlogPost, 'id' | 'title' | 'author' | 'publishedAt'>

// Create discriminated unions
type APIResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string }

// Usage
const updatePost = async (id: string, update: BlogPostUpdate): Promise<APIResponse<BlogPost>> => {
  try {
    const updatedPost = await $fetch<BlogPost>(`/api/posts/${id}`, {
      method: 'PATCH',
      body: update
    })
    return { success: true, data: updatedPost }
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error.message : 'Update failed' 
    }
  }
}

Testing with TypeScript

Type-Safe Test Setup

// tests/utils/testHelpers.ts
export const createMockBlogPost = (overrides: Partial<BlogPost> = {}): BlogPost => {
  return {
    id: 'test-id',
    title: 'Test Post',
    content: 'Test content',
    author: {
      name: 'Test Author',
      email: 'test@example.com'
    },
    publishedAt: new Date(),
    tags: ['test'],
    ...overrides
  }
}

export const createMockAuthor = (overrides: Partial<Author> = {}): Author => {
  return {
    name: 'Test Author',
    email: 'test@example.com',
    ...overrides
  }
}
// tests/components/BlogPost.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import BlogPost from '~/components/BlogPost.vue'
import { createMockBlogPost } from '~/tests/utils/testHelpers'

describe('BlogPost Component', () => {
  it('renders blog post correctly', () => {
    const mockPost = createMockBlogPost({
      title: 'Custom Test Title',
      author: {
        name: 'Custom Author',
        email: 'custom@example.com'
      }
    })

    const wrapper = mount(BlogPost, {
      props: { post: mockPost }
    })

    expect(wrapper.find('h1').text()).toBe('Custom Test Title')
    expect(wrapper.find('.author-info span').text()).toBe('Custom Author')
  })
})

Common TypeScript Pitfalls

1. Avoiding any

// ❌ Bad - loses type safety
const processData = (data: any) => {
  return data.someProperty
}

// ✅ Good - use generics or specific types
const processData = <T extends { someProperty: unknown }>(data: T): T['someProperty'] => {
  return data.someProperty
}

2. Proper Error Handling

// ❌ Bad - catching unknown errors
try {
  await fetchData()
} catch (error) {
  console.log(error.message) // TypeScript error
}

// ✅ Good - type-safe error handling
try {
  await fetchData()
} catch (error) {
  const message = error instanceof Error ? error.message : 'Unknown error'
  console.log(message)
}

3. Working with Vue Refs

// ❌ Bad - implicit any
const element = ref()

// ✅ Good - explicit typing
const element = ref<HTMLElement | null>(null)
const posts = ref<BlogPost[]>([])

Conclusion

TypeScript significantly improves the development experience when building Vue applications. By following these best practices, you can:

  • Catch errors at compile time
  • Improve IDE support and autocomplete
  • Make your code more maintainable
  • Enhance team collaboration

Remember to:

  • Use strict TypeScript settings
  • Define clear interfaces for your data
  • Leverage utility types and generics
  • Write type-safe tests
  • Avoid any whenever possible

With these patterns, you'll build more robust and maintainable Vue applications! 🎯