Skip to content

TypeScript Usage Guide

Complete solution for type-safe development in Vant + Vue 3 projects.

🎯 TypeScript Advantages

Core Benefits

  • Type Safety: Compile-time error checking, reducing runtime bugs
  • Development Efficiency: Intelligent hints and auto-completion, improving development experience
  • Refactoring Friendly: Safe code refactoring, reducing maintenance costs
  • Documentation: Types serve as documentation, improving code readability

🚀 Quick Start

Project Initialization

bash
# Use official Vite template
npm create vue@latest my-vant-ts-app

# Interactive configuration selection
 Project name: my-vant-ts-app
 Add TypeScript? Yes
 Add JSX Support? No  
 Add Vue Router? Yes
 Add Pinia? Yes
 Add Vitest? Yes
 Add ESLint? Yes
 Add Prettier? Yes

# Install Vant dependencies
cd my-vant-ts-app
npm install vant @vant/touch-emulator

# Install development dependencies
npm install -D @types/node unplugin-vue-components

⚙️ Core Configuration

Vite Configuration

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
import { resolve } from 'path'

export default defineConfig({
  plugins: [
    vue({
      script: {
        defineModel: true,
        propsDestructure: true
      }
    }),
    Components({
      resolvers: [VantResolver()],
      dts: true,
      dirs: ['src/components'],
      extensions: ['vue', 'ts'],
      deep: true
    })
  ],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@types': resolve(__dirname, 'src/types'),
      '@api': resolve(__dirname, 'src/api')
    }
  },
  
  server: {
    port: 3000,
    open: true,
    cors: true
  },
  
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'vant-vendor': ['vant']
        }
      }
    }
  }
})

TypeScript Configuration

json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": [
    "env.d.ts",
    "src/**/*",
    "src/**/*.vue",
    "tests/**/*"
  ],
  "exclude": [
    "dist",
    "node_modules"
  ],
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "bundler",
    
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"],
      "@api/*": ["src/api/*"]
    },
    
    "jsx": "preserve",
    "jsxImportSource": "vue",
    
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Environment Type Declarations

typescript
// env.d.ts
/// <reference types="vite/client" />

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

declare module 'vant' {
  export * from 'vant/es'
}

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_APP_VERSION: string
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_ENV: 'development' | 'production' | 'test'
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

declare global {
  interface Window {
    __VUE_DEVTOOLS_GLOBAL_HOOK__?: any
    gtag?: (...args: any[]) => void
    dataLayer?: any[]
  }
  
  interface DocumentEventMap {
    'app:ready': CustomEvent<{ version: string }>
    'user:login': CustomEvent<{ userId: string }>
  }
}

export {}

📝 Type System Design

Basic Business Types

typescript
// types/user.ts
export interface BaseUser {
  readonly id: string
  name: string
  email: string
  avatar?: string
  createdAt: Date
  updatedAt: Date
}

export interface User extends BaseUser {
  phone?: string
  status: UserStatus
  role: UserRole
  profile: UserProfile
  preferences: UserPreferences
}

export type UserStatus = 'active' | 'inactive' | 'pending' | 'suspended'
export type UserRole = 'admin' | 'user' | 'guest' | 'moderator'

export interface UserProfile {
  firstName: string
  lastName: string
  bio?: string
  location?: string
  website?: string
  socialLinks: SocialLinks
}

export interface SocialLinks {
  github?: string
  twitter?: string
  linkedin?: string
}

export interface UserPreferences {
  theme: 'light' | 'dark' | 'auto'
  language: 'zh-CN' | 'en-US'
  notifications: NotificationSettings
}

export interface NotificationSettings {
  email: boolean
  push: boolean
  sms: boolean
  categories: NotificationCategory[]
}

export type NotificationCategory = 
  | 'system' 
  | 'security' 
  | 'marketing' 
  | 'updates'

// Type guard functions
export function isValidUser(obj: any): obj is User {
  return (
    typeof obj === 'object' &&
    typeof obj.id === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    obj.createdAt instanceof Date
  )
}

API Response Types

typescript
// types/api.ts
export interface ApiResponse<T = unknown> {
  readonly code: number
  readonly message: string
  readonly data: T
  readonly timestamp: number
  readonly requestId: string
}

export interface ApiError {
  readonly code: number
  readonly message: string
  readonly details?: Record<string, any>
  readonly field?: string
}

export interface PaginationParams {
  page: number
  pageSize: number
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
}

export interface PaginationResponse<T> {
  list: T[]
  total: number
  page: number
  pageSize: number
  totalPages: number
  hasNext: boolean
  hasPrev: boolean
}

export interface ListResponse<T> extends ApiResponse<PaginationResponse<T>> {}

export interface RequestConfig {
  timeout?: number
  retries?: number
  showLoading?: boolean
  showError?: boolean
  errorHandler?: (error: ApiError) => void
}

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
export type RequestParams = Record<string, any>
export type RequestData = Record<string, any> | FormData

export type ResponseInterceptor<T = any> = (
  response: ApiResponse<T>
) => ApiResponse<T> | Promise<ApiResponse<T>>

Component Props Types

typescript
// types/components.ts
import type { VNode, Component } from 'vue'

export interface BaseComponentProps {
  id?: string
  class?: string | string[] | Record<string, boolean>
  style?: string | Record<string, string | number>
}

export type ComponentSize = 'mini' | 'small' | 'normal' | 'large'
export type ComponentTheme = 
  | 'primary' 
  | 'success' 
  | 'warning' 
  | 'danger' 
  | 'default'

export interface ButtonProps extends BaseComponentProps {
  type?: ComponentTheme
  size?: ComponentSize
  loading?: boolean
  disabled?: boolean
  round?: boolean
  square?: boolean
  icon?: string | Component
  iconPosition?: 'left' | 'right'
  nativeType?: 'button' | 'submit' | 'reset'
  block?: boolean
  text?: boolean
  color?: string
  loadingText?: string
  loadingType?: 'circular' | 'spinner'
  loadingSize?: string | number
}

export interface FormItemProps extends BaseComponentProps {
  label?: string
  name?: string
  required?: boolean
  rules?: ValidationRule[]
  error?: string
  labelWidth?: string | number
  labelAlign?: 'left' | 'center' | 'right'
  inputAlign?: 'left' | 'center' | 'right'
  colon?: boolean
}

export interface ValidationRule {
  required?: boolean
  message?: string
  pattern?: RegExp
  min?: number
  max?: number
  len?: number
  validator?: (value: any, rule: ValidationRule) => boolean | string | Promise<boolean | string>
  trigger?: 'onChange' | 'onBlur' | 'onSubmit'
}

export type EventHandler<T = Event> = (event: T) => void
export type AsyncEventHandler<T = Event> = (event: T) => Promise<void>

export type SlotContent = string | number | VNode | VNode[]
export type ScopedSlot<T = any> = (props: T) => SlotContent

🎯 Vue 3 + Vant Type Practices

Advanced Component Type Definition

vue
<!-- UserCard.vue -->
<script setup lang="ts">
import type { User, UserStatus } from '@/types/user'
import type { ComponentSize } from '@/types/components'

interface Props {
  user: User
  size?: ComponentSize
  showActions?: boolean
  showStatus?: boolean
  clickable?: boolean
  loading?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  size: 'normal',
  showActions: true,
  showStatus: true,
  clickable: false,
  loading: false
})

interface Emits {
  (e: 'click', user: User): void
  (e: 'edit', userId: string): void
  (e: 'delete', userId: string): void
  (e: 'statusChange', userId: string, status: UserStatus): void
}

const emit = defineEmits<Emits>()

const displayName = computed((): string => {
  const { firstName, lastName } = props.user.profile
  return `${firstName} ${lastName}`.trim() || props.user.name
})

const statusColor = computed((): string => {
  const statusMap: Record<UserStatus, string> = {
    active: '#07c160',
    inactive: '#969799',
    pending: '#ff976a',
    suspended: '#ee0a24'
  }
  return statusMap[props.user.status]
})

const cardClass = computed(() => [
  'user-card',
  `user-card--${props.size}`,
  {
    'user-card--clickable': props.clickable,
    'user-card--loading': props.loading
  }
])

const handleClick = (): void => {
  if (props.clickable && !props.loading) {
    emit('click', props.user)
  }
}

const handleEdit = (event: Event): void => {
  event.stopPropagation()
  emit('edit', props.user.id)
}

const handleDelete = (event: Event): void => {
  event.stopPropagation()
  emit('delete', props.user.id)
}

const handleStatusChange = (newStatus: UserStatus): void => {
  emit('statusChange', props.user.id, newStatus)
}

defineExpose({
  refresh: () => {
    // User data refresh logic
  }
})
</script>

<template>
  <van-card
    :class="cardClass"
    :title="displayName"
    :desc="user.email"
    :thumb="user.avatar"
    @click="handleClick"
  >
    <template #tags v-if="showStatus">
      <van-tag 
        :color="statusColor" 
        size="small"
        @click="handleStatusChange"
      >
        {{ user.status }}
      </van-tag>
    </template>
    
    <template #footer v-if="showActions">
      <div class="user-card__actions">
        <van-button 
          size="small" 
          type="primary"
          :loading="loading"
          @click="handleEdit"
        >
          Edit
        </van-button>
        <van-button 
          size="small" 
          type="danger"
          :loading="loading"
          @click="handleDelete"
        >
          Delete
        </van-button>
      </div>
    </template>
  </van-card>
</template>

<style scoped lang="scss">
.user-card {
  &--clickable {
    cursor: pointer;
    transition: all 0.3s ease;
    
    &:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    }
  }
  
  &--loading {
    opacity: 0.6;
    pointer-events: none;
  }
  
  &__actions {
    display: flex;
    gap: 8px;
  }
}
</style>

State Management Type Definition

typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, UserStatus } from '@/types/user'
import type { ApiResponse, PaginationParams, ListResponse } from '@/types/api'
import { userApi } from '@/api/user'

interface UserState {
  currentUser: User | null
  users: User[]
  loading: boolean
  error: string | null
  pagination: {
    page: number
    pageSize: number
    total: number
  }
}

export const useUserStore = defineStore('user', () => {
  // State definition
  const currentUser = ref<User | null>(null)
  const users = ref<User[]>([])
  const loading = ref<boolean>(false)
  const error = ref<string | null>(null)
  const pagination = ref({
    page: 1,
    pageSize: 20,
    total: 0
  })

  // Computed properties (Getters)
  const isLoggedIn = computed((): boolean => {
    return currentUser.value !== null
  })

  const activeUsers = computed((): User[] => {
    return users.value.filter(user => user.status === 'active')
  })

  const userCount = computed((): number => {
    return users.value.length
  })

  const hasNextPage = computed((): boolean => {
    const { page, pageSize, total } = pagination.value
    return page * pageSize < total
  })

  const hasPrevPage = computed((): boolean => {
    return pagination.value.page > 1
  })

  // Actions
  const fetchUsers = async (params?: PaginationParams): Promise<void> => {
    try {
      loading.value = true
      error.value = null

      const response: ListResponse<User> = await userApi.getUsers(params)

      if (response.code === 200) {
        users.value = response.data.list
        pagination.value = {
          page: response.data.page,
          pageSize: response.data.pageSize,
          total: response.data.total
        }
      } else {
        throw new Error(response.message)
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to fetch user list'
      console.error('fetchUsers error:', err)
    } finally {
      loading.value = false
    }
  }

  const fetchUserById = async (id: string): Promise<User | null> => {
    try {
      loading.value = true
      const response: ApiResponse<User> = await userApi.getUserById(id)
      
      if (response.code === 200) {
        return response.data
      } else {
        throw new Error(response.message)
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to fetch user details'
      return null
    } finally {
      loading.value = false
    }
  }

  const createUser = async (
    userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>
  ): Promise<boolean> => {
    try {
      loading.value = true
      const response: ApiResponse<User> = await userApi.createUser(userData)

      if (response.code === 200) {
        users.value.unshift(response.data)
        pagination.value.total += 1
        return true
      } else {
        throw new Error(response.message)
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to create user'
      return false
    } finally {
      loading.value = false
    }
  }

  const updateUser = async (id: string, userData: Partial<User>): Promise<boolean> => {
    try {
      loading.value = true
      const response: ApiResponse<User> = await userApi.updateUser(id, userData)

      if (response.code === 200) {
        const index = users.value.findIndex(user => user.id === id)
        if (index !== -1) {
          users.value[index] = response.data
        }
        if (currentUser.value?.id === id) {
          currentUser.value = response.data
        }
        return true
      } else {
        throw new Error(response.message)
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to update user'
      return false
    } finally {
      loading.value = false
    }
  }

  const deleteUser = async (id: string): Promise<boolean> => {
    try {
      loading.value = true
      const response: ApiResponse<void> = await userApi.deleteUser(id)

      if (response.code === 200) {
        users.value = users.value.filter(user => user.id !== id)
        pagination.value.total -= 1
        return true
      } else {
        throw new Error(response.message)
      }
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to delete user'
      return false
    } finally {
      loading.value = false
    }
  }

  const setCurrentUser = (user: User | null): void => {
    currentUser.value = user
  }

  const updateUserStatus = async (id: string, status: UserStatus): Promise<boolean> => {
    return updateUser(id, { status })
  }

  const clearError = (): void => {
    error.value = null
  }

  const reset = (): void => {
    currentUser.value = null
    users.value = []
    loading.value = false
    error.value = null
    pagination.value = { page: 1, pageSize: 20, total: 0 }
  }

  return {
    // State
    currentUser: readonly(currentUser),
    users: readonly(users),
    loading: readonly(loading),
    error: readonly(error),
    pagination: readonly(pagination),

    // Computed properties
    isLoggedIn,
    activeUsers,
    userCount,
    hasNextPage,
    hasPrevPage,

    // Methods
    fetchUsers,
    fetchUserById,
    createUser,
    updateUser,
    deleteUser,
    setCurrentUser,
    updateUserStatus,
    clearError,
    reset
  }
})

export type UserStore = ReturnType<typeof useUserStore>

🔧 Type-safe API Request Encapsulation

Type-safe HTTP Client

typescript
// utils/request.ts
import axios, { 
  AxiosInstance, 
  AxiosRequestConfig, 
  AxiosResponse, 
  AxiosError 
} from 'axios'
import type { ApiResponse, ApiError, RequestConfig } from '@/types/api'
import { showToast, showLoadingToast, closeToast } from 'vant'

interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
  showLoading?: boolean
  showError?: boolean
  retries?: number
  retryDelay?: number
}

class HttpClient {
  private instance: AxiosInstance
  private loadingCount = 0

  constructor(baseURL: string) {
    this.instance = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })

    this.setupInterceptors()
  }

  private setupInterceptors(): void {
    // Request interceptor
    this.instance.interceptors.request.use(
      (config: ExtendedAxiosRequestConfig) => {
        if (config.showLoading) {
          this.showLoading()
        }

        const token = localStorage.getItem('token')
        if (token) {
          config.headers = {
            ...config.headers,
            Authorization: `Bearer ${token}`
          }
        }

        return config
      },
      (error: AxiosError) => {
        this.hideLoading()
        return Promise.reject(error)
      }
    )

    // Response interceptor
    this.instance.interceptors.response.use(
      (response: AxiosResponse<ApiResponse>) => {
        this.hideLoading()
        
        const { data } = response
        
        if (data.code !== 200) {
          const error: ApiError = {
            code: data.code,
            message: data.message
          }
          
          if (response.config.showError !== false) {
            showToast(error.message)
          }
          
          return Promise.reject(error)
        }

        return response
      },
      async (error: AxiosError) => {
        this.hideLoading()
        
        const config = error.config as ExtendedAxiosRequestConfig
        
        if (config?.retries && config.retries > 0) {
          config.retries -= 1
          await this.delay(config.retryDelay || 1000)
          return this.instance.request(config)
        }

        const apiError: ApiError = {
          code: error.response?.status || 0,
          message: this.getErrorMessage(error)
        }

        if (config?.showError !== false) {
          showToast(apiError.message)
        }

        return Promise.reject(apiError)
      }
    )
  }

  private showLoading(): void {
    if (this.loadingCount === 0) {
      showLoadingToast({
        message: 'Loading...',
        forbidClick: true,
        duration: 0
      })
    }
    this.loadingCount++
  }

  private hideLoading(): void {
    this.loadingCount--
    if (this.loadingCount <= 0) {
      this.loadingCount = 0
      closeToast()
    }
  }

  private getErrorMessage(error: AxiosError): string {
    if (error.response) {
      const status = error.response.status
      const statusMessages: Record<number, string> = {
        400: 'Bad Request',
        401: 'Unauthorized, please login again',
        403: 'Access Denied',
        404: 'Requested resource not found',
        408: 'Request Timeout',
        500: 'Internal Server Error',
        502: 'Bad Gateway',
        503: 'Service Unavailable',
        504: 'Gateway Timeout'
      }
      return statusMessages[status] || `Request failed (${status})`
    } else if (error.request) {
      return 'Network connection failed, please check your network'
    } else {
      return error.message || 'Unknown error'
    }
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  async request<T = any>(config: ExtendedAxiosRequestConfig): Promise<ApiResponse<T>> {
    const response = await this.instance.request<ApiResponse<T>>(config)
    return response.data
  }

  async get<T = any>(
    url: string,
    params?: any,
    config?: RequestConfig
  ): Promise<ApiResponse<T>> {
    return this.request<T>({
      method: 'GET',
      url,
      params,
      ...config
    })
  }

  async post<T = any>(
    url: string,
    data?: any,
    config?: RequestConfig
  ): Promise<ApiResponse<T>> {
    return this.request<T>({
      method: 'POST',
      url,
      data,
      ...config
    })
  }

  async put<T = any>(
    url: string,
    data?: any,
    config?: RequestConfig
  ): Promise<ApiResponse<T>> {
    return this.request<T>({
      method: 'PUT',
      url,
      data,
      ...config
    })
  }

  async delete<T = any>(
    url: string,
    config?: RequestConfig
  ): Promise<ApiResponse<T>> {
    return this.request<T>({
      method: 'DELETE',
      url,
      ...config
    })
  }
}

const httpClient = new HttpClient(import.meta.env.VITE_API_BASE_URL)

export default httpClient

📚 Best Practices Summary

Type Safety Checklist

  • ✅ Enable strict: true and related strict checks
  • ✅ Use import type for type imports
  • ✅ Properly use generics and type constraints
  • ✅ Implement runtime type checking functions
  • ✅ Use readonly to protect immutable data
  • ✅ Use union types instead of any
  • ✅ Make good use of utility types like Partial, Pick, Omit
  • ✅ Use type guards carefully, prefer type assertions

Performance Optimization Suggestions

Compilation Performance

  • Use skipLibCheck to skip library type checking
  • Properly configure include/exclude
  • Use incremental compilation
  • Avoid overly deep type nesting

Bundle Optimization

  • Type files don't affect runtime
  • Use Tree Shaking to remove unused code
  • Proper chunking to avoid type pollution
  • Remove type checking in production

Through the above TypeScript configuration and practices, you can enjoy a complete type-safe development experience in Vant + Vue 3 projects.

Enterprise-level mobile solution based on Vant