Advanced TypeScript Patterns for Vue Developers

Advanced TypeScript Patterns for Vue Developers

TypeScript with Vue

TypeScript has become an essential tool for Vue developers who want to build more reliable and maintainable applications. In this article, we'll explore advanced patterns that can take your Vue + TypeScript skills to the next level.

Type-Safe Component Props

Generic Components with Type Parameters

Create reusable components that maintain type safety across different data types:

<script setup lang="ts" generic="T extends { id: string | number }">
import type { PropType } from 'vue'

const props = defineProps({
  items: {
    type: Array as PropType<T[]>,
    required: true
  },
  selectedId: {
    type: [String, Number] as PropType<string | number>,
    default: null
  }
})

// Type-safe computed property
const selectedItem = computed(() => 
  props.items.find(item => item.id === props.selectedId)
)
</script>

Discriminated Unions for Component Variants

Use TypeScript's discriminated unions to create type-safe variant components:

type ButtonVariant = 
  | { type: 'primary'; icon?: string }
  | { type: 'secondary'; outline: boolean }
  | { type: 'danger'; confirmText: string }

const props = defineProps<{
  variant: ButtonVariant
}>()

// TypeScript knows which properties are available based on variant.type
const buttonClass = computed(() => {
  switch (props.variant.type) {
    case 'primary':
      return 'btn-primary'
    case 'secondary':
      return props.variant.outline ? 'btn-outline' : 'btn-secondary'
    case 'danger':
      return 'btn-danger'
  }
})

Advanced Composables with Type Inference

Type-Safe Store Patterns

Create a type-safe store pattern using composables:

// stores/useUserStore.ts
interface User {
  id: string
  name: string
  email: string
  preferences: UserPreferences
}

interface UserPreferences {
  theme: 'light' | 'dark'
  notifications: boolean
}

export const useUserStore = () => {
  const user = ref<User | null>(null)
  const isLoading = ref(false)
  const error = ref<Error | null>(null)

  const fetchUser = async (userId: string) => {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await $fetch<User>(`/api/users/${userId}`)
      user.value = response
    } catch (err) {
      error.value = err as Error
    } finally {
      isLoading.value = false
    }
  }

  // Computed properties with proper typing
  const userName = computed(() => user.value?.name ?? 'Guest')
  const isDarkTheme = computed(() => 
    user.value?.preferences.theme === 'dark'
  )

  return {
    user: readonly(user),
    isLoading: readonly(isLoading),
    error: readonly(error),
    userName,
    isDarkTheme,
    fetchUser
  }
}

Generic Data Fetching Composable

Create a reusable data fetching composable with full TypeScript support:

// composables/useFetchData.ts
export function useFetchData<T>(url: string | Ref<string>, options?: {
  immediate?: boolean
  transform?: (data: any) => T
}) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  const execute = async () => {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await $fetch(unref(url))
      data.value = options?.transform 
        ? options.transform(response) 
        : response as T
    } catch (err) {
      error.value = err as Error
    } finally {
      isLoading.value = false
    }
  }

  if (options?.immediate !== false) {
    execute()
  }

  return {
    data: readonly(data),
    error: readonly(error),
    isLoading: readonly(isLoading),
    execute
  }
}

// Usage with type safety
const { data: articles, isLoading } = useFetchData<Article[]>('/api/articles', {
  transform: (response) => response.data
})

Template Ref Typing

Type-Safe Template Refs

Properly type template refs for better IDE support:

<template>
  <div ref="containerRef">
    <!-- Content -->
  </div>
  <input ref="inputRef" type="text" />
</template>

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

// Properly typed template refs
const containerRef = ref<HTMLDivElement>()
const inputRef = ref<HTMLInputElement>()

onMounted(() => {
  // TypeScript knows these are HTML elements
  containerRef.value?.style.backgroundColor = 'blue'
  inputRef.value?.focus()
})
</script>

Component Instance Typing

Type component instances when using refs with child components:

// Child component
const emit = defineEmits<{
  (e: 'update:modelValue', value: string): void
  (e: 'submit', payload: FormData): void
}>()

// Expose methods for parent access
defineExpose({
  validate: () => boolean,
  reset: () => void
})

// Parent component
const childRef = ref<InstanceType<typeof ChildComponent>>()

const validateForm = () => {
  // TypeScript knows about exposed methods
  const isValid = childRef.value?.validate()
  if (isValid) {
    childRef.value?.reset()
  }
}

Type-Safe Route Params and Query

Route Param Typing

Create type-safe route parameters:

// types/routes.ts
export type RouteParams = {
  '/articles/[slug]': { slug: string }
  '/users/[id]/edit': { id: string }
  '/search': { q?: string; page?: string }
}

// composables/useTypedRoute.ts
import type { RouteParams } from '~/types/routes'

export function useTypedRoute<T extends keyof RouteParams>(routeName: T) {
  const route = useRoute()
  
  // Type-safe params and query
  const params = route.params as RouteParams[T]
  const query = route.query as Partial<RouteParams[T]>
  
  return { params, query, route }
}

// Usage
const { params } = useTypedRoute('/articles/[slug]')
// params.slug is typed as string
console.log(params.slug)

Utility Types for Vue

Create Custom Utility Types

Build utility types specific to Vue patterns:

// types/vue-utils.ts
import type { Component, ComponentPublicInstance } from 'vue'

// Extract props from a component
type ExtractComponentProps<T extends Component> = 
  T extends new () => ComponentPublicInstance<infer P> ? P : never

// Extract emits from a component  
type ExtractComponentEmits<T extends Component> =
  T extends new () => ComponentPublicInstance<any, infer E> ? E : never

// Usage with a component
import MyButton from '~/components/MyButton.vue'

type ButtonProps = ExtractComponentProps<typeof MyButton>
type ButtonEmits = ExtractComponentEmits<typeof MyButton>

Testing with Type Safety

Type-Safe Test Utilities

Create type-safe testing utilities:

// test-utils.ts
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'

export function typedMount<T extends Component>(
  component: T,
  options?: Parameters<typeof mount<T>>[1]
): VueWrapper<ComponentPublicInstance<ExtractComponentProps<T>>> {
  return mount(component, options)
}

// Usage in tests
const wrapper = typedMount(MyComponent, {
  props: {
    // TypeScript will error if props don't match
    title: 'Test Title',
    count: 42
  }
})

Performance Considerations

Type-Only Imports

Use type-only imports to reduce bundle size:

// Good - type-only import
import type { User } from '~/types/user'

// Good - runtime import
import { fetchUser } from '~/api/users'

// Avoid - mixed import (increases bundle size)
import { User, fetchUser } from '~/api/users'

Conditional Types for Dynamic Components

Use conditional types for dynamic component loading:

type ComponentMap = {
  'text': typeof TextComponent
  'image': typeof ImageComponent
  'video': typeof VideoComponent
}

const componentType = ref<keyof ComponentMap>('text')

const CurrentComponent = computed(() => 
  defineAsyncComponent(() => 
    import(`~/components/${componentType.value}.vue`)
      .then(module => module.default)
  )
)

Conclusion

Mastering advanced TypeScript patterns in Vue development leads to:

  1. Fewer Runtime Errors: Catch issues at compile time
  2. Better Developer Experience: Improved IDE support and autocomplete
  3. More Maintainable Code: Self-documenting types and interfaces
  4. Easier Refactoring: Type safety makes large-scale changes safer

Start incorporating these patterns into your Vue projects today to build more robust and maintainable applications.

Sources and References