typescript 精选推荐

TypeScript 在 Vue 3 中的最佳实践

HTMLPAGE 团队
12 分钟阅读

深度讲解如何在 Vue 3 中高效使用 TypeScript,包括类型定义、接口设计、generics 应用、常见错误等完整指南。

#TypeScript #Vue 3 #类型安全 #最佳实践 #接口设计

TypeScript 在 Vue 3 中的最佳实践

TypeScript 让 Vue 开发更加安全可靠。本文讲解如何在 Vue 3 中高效使用 TypeScript。

1. 基础类型定义

组件 Props 的类型定义

// ✅ 完整的类型定义

import { PropType } from 'vue'

interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

interface Props {
  // 必需的属性
  title: string
  
  // 可选属性
  count?: number
  
  // 对象类型
  user?: User
  
  // 数组类型
  items?: (string | number)[]
  
  // 函数类型
  onSubmit?: (data: any) => void
  
  // 字面量类型
  size?: 'sm' | 'md' | 'lg'
  
  // any 类型 (避免)
  // data?: any
}

export default {
  props: {
    title: {
      type: String,
      required: true
    },
    
    count: {
      type: Number,
      default: 0
    },
    
    user: {
      type: Object as PropType<User>,
      default: () => ({})
    },
    
    size: {
      type: String,
      default: 'md',
      validator: (value: string) => ['sm', 'md', 'lg'].includes(value)
    }
  }
}

// 在 <script setup> 中
<script setup lang="ts">
interface Props {
  title: string
  count?: number
}

withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

组件 Emits 的类型定义

// ✅ 类型安全的事件发射

interface Emits {
  (e: 'submit', data: { name: string; email: string }): void
  (e: 'cancel'): void
  (e: 'delete', id: string): void
}

// 选项式 API
export default {
  emits: {
    submit: (data: { name: string; email: string }) => {
      // 可选: 验证
      return data.name && data.email
    },
    cancel: null,
    delete: (id: string) => id !== ''
  }
}

// <script setup>
<script setup lang="ts">
const emit = defineEmits<{
  submit: [data: { name: string; email: string }]
  cancel: []
  delete: [id: string]
}>()

const handleSubmit = () => {
  emit('submit', { name: 'Alice', email: 'alice@example.com' })
}
</script>

2. 高级类型模式

泛型组件

// components/List.vue
<script setup lang="ts" generic="T extends Record<string, any>">
interface Props {
  items: T[]
  keyField?: keyof T
}

const props = withDefaults(defineProps<Props>(), {
  keyField: 'id' as keyof T
})

const emit = defineEmits<{
  select: [item: T]
}>()
</script>

<template>
  <div>
    <div
      v-for="item in items"
      :key="item[keyField]"
      @click="emit('select', item)"
    >
      {{ item }}
    </div>
  </div>
</template>

// 使用泛型组件
<script setup lang="ts">
interface User {
  id: string
  name: string
}

const users = ref<User[]>([
  { id: '1', name: 'Alice' },
  { id: '2', name: 'Bob' }
])

const handleSelect = (user: User) => {
  console.log('选中:', user.name)
}
</script>

<template>
  <!-- ✅ 类型完全推断 -->
  <List
    :items="users"
    key-field="id"
    @select="handleSelect"
  />
</template>

条件类型和分布式条件类型

// 条件类型 (Conditional Types)

// 基础条件类型
type IsString<T> = T extends string ? true : false

type A = IsString<'hello'>  // true
type B = IsString<number>   // false

// 分布式条件类型
type Flatten<T> = T extends Array<infer U> ? U : T

type Str = Flatten<string[]>        // string
type Num = Flatten<number>          // number
type Mixed = Flatten<(string | number)[]>  // string | number

// 实战: 提取 Promise 的结果类型
type Awaited<T> = T extends Promise<infer U> ? U : T

type Result = Awaited<Promise<string>>  // string

// 实战: API 响应类型
interface APIResponse<T> {
  code: number
  message: string
  data: T
}

type ExtractData<T> = T extends APIResponse<infer D> ? D : never

type UserData = ExtractData<APIResponse<{ name: string }>>  // { name: string }

复杂的接口设计

// 实战: 表单验证框架

// 1. 定义字段验证规则
interface FieldRule {
  required?: boolean
  minLength?: number
  maxLength?: number
  pattern?: RegExp
  custom?: (value: any) => boolean | string
}

// 2. 为每个字段定义规则
interface FormSchema {
  [fieldName: string]: FieldRule
}

// 3. 带验证的表单处理
class FormValidator<T extends Record<string, any>> {
  constructor(private schema: FormSchema) {}
  
  validate(data: T): Record<keyof T, string[]> {
    const errors: Record<string, string[]> = {}
    
    for (const [field, rules] of Object.entries(this.schema)) {
      const errors_list: string[] = []
      const value = data[field]
      
      if (rules.required && !value) {
        errors_list.push(`${field} 是必需的`)
      }
      
      if (rules.minLength && value?.length < rules.minLength) {
        errors_list.push(`${field} 至少需要 ${rules.minLength} 个字符`)
      }
      
      if (errors_list.length > 0) {
        errors[field] = errors_list
      }
    }
    
    return errors as Record<keyof T, string[]>
  }
}

// 使用
interface LoginForm {
  email: string
  password: string
}

const validator = new FormValidator<LoginForm>({
  email: { required: true, pattern: /^.+@.+\..+$/ },
  password: { required: true, minLength: 6 }
})

const errors = validator.validate({
  email: 'invalid',
  password: '123'
})

3. 组合式 API 的类型定义

Composable 的返回类型

// composables/useCounter.ts

import { ref, computed, Ref } from 'vue'

// 定义返回类型
interface UseCounterReturn {
  count: Ref<number>
  double: ComputedRef<number>
  increment: () => void
  reset: () => void
}

// 实现 composable
export const useCounter = (initialValue: number = 0): UseCounterReturn => {
  const count = ref(initialValue)
  
  const double = computed(() => count.value * 2)
  
  const increment = () => count.value++
  
  const reset = () => count.value = initialValue
  
  return {
    count,
    double,
    increment,
    reset
  }
}

// 使用时自动推断类型
<script setup lang="ts">
const { count, double, increment } = useCounter(10)
// count: Ref<number>
// double: ComputedRef<number>
// increment: () => void
</script>

Composable 的泛型

// composables/useFetch.ts

interface UseFetchReturn<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  refresh: () => Promise<void>
}

export const useFetch = async <T = any>(
  url: string | Ref<string>,
  options?: FetchOptions
): Promise<UseFetchReturn<T>> => {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  const refresh = async () => {
    loading.value = true
    try {
      const response = await $fetch<T>(url, options)
      data.value = response
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }
  
  await refresh()
  
  return { data, loading, error, refresh }
}

// 使用
<script setup lang="ts">
interface User {
  id: string
  name: string
  email: string
}

const { data: users, loading } = await useFetch<User[]>('/api/users')
// users: Ref<User[] | null>
// loading: Ref<boolean>
</script>

4. 常见类型错误和解决方案

错误 1: Any 类型滥用

// ❌ 避免
const handleClick = (event: any) => {
  console.log(event.target.value)  // 无类型检查
}

const fetchData = async (url: any) => {
  const response = await $fetch(url)
  return response  // any 类型
}

// ✅ 正确
const handleClick = (event: MouseEvent) => {
  if (event.target instanceof HTMLInputElement) {
    console.log(event.target.value)
  }
}

interface FetchOptions {
  method?: 'GET' | 'POST'
  headers?: Record<string, string>
  body?: Record<string, any>
}

const fetchData = async (url: string, options?: FetchOptions) => {
  const response = await $fetch(url, options)
  return response
}

错误 2: 类型断言滥用

// ❌ 避免 (类型断言隐藏问题)
const data = response as User[]
const count = element as HTMLInputElement

// ✅ 正确 (类型保护)
const isUserArray = (data: unknown): data is User[] => {
  return Array.isArray(data) && data.every(item => 'id' in item)
}

if (isUserArray(response)) {
  // 现在 response 被确定为 User[]
}

const parseElement = (element: Element): HTMLInputElement | null => {
  if (element instanceof HTMLInputElement) {
    return element
  }
  return null
}

错误 3: Props 类型和运行时定义不匹配

// ❌ 避免 (类型和运行时不一致)
interface Props {
  user: User | null
}

export default {
  props: {
    user: String  // ❌ 类型是 object,运行时是 string!
  }
}

// ✅ 正确
interface Props {
  user?: User | null
}

export default {
  props: {
    user: {
      type: Object as PropType<User | null>,
      default: null
    }
  }
}

5. Pinia Store 的类型定义

// stores/useUserStore.ts

interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user'
}

interface UserState {
  users: User[]
  currentUser: User | null
  loading: boolean
  error: string | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    users: [],
    currentUser: null,
    loading: false,
    error: null
  }),
  
  getters: {
    isAdmin: (state) => state.currentUser?.role === 'admin',
    
    getUserById: (state) => (id: string): User | undefined => {
      return state.users.find(user => user.id === id)
    }
  },
  
  actions: {
    async fetchUsers(): Promise<void> {
      this.loading = true
      try {
        const users = await $fetch<User[]>('/api/users')
        this.users = users
      } catch (error: any) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    
    setCurrentUser(user: User | null): void {
      this.currentUser = user
    }
  }
})

// 使用
<script setup lang="ts">
const userStore = useUserStore()

// 所有都有完整的类型提示
const { currentUser, isAdmin } = storeToRefs(userStore)
const user = userStore.getUserById('123')  // User | undefined
</script>

6. API 响应的类型定义

// types/api.ts

// 通用 API 响应格式
interface APIResponse<T> {
  code: number
  message: string
  data: T
  timestamp: number
}

// 分页响应
interface PaginatedResponse<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
  hasMore: boolean
}

// 具体的 API 类型

interface User {
  id: string
  name: string
  email: string
  avatar?: string
  createdAt: string
}

interface UserResponse extends APIResponse<User> {}

interface UsersListResponse extends APIResponse<PaginatedResponse<User>> {}

type CreateUserRequest = Omit<User, 'id' | 'createdAt'>

// API 调用类型安全
<script setup lang="ts">
const getUserList = async (): Promise<UsersListResponse> => {
  return await $fetch('/api/users')
}

const createUser = async (data: CreateUserRequest): Promise<UserResponse> => {
  return await $fetch('/api/users', {
    method: 'POST',
    body: data
  })
}

// 使用
const response = await getUserList()
// response: UsersListResponse
// response.data.items: User[]
</script>

7. 类型工具函数

// types/utils.ts

// 1. 提取对象键的联合类型
type Keys<T> = keyof T
type UserKeys = Keys<User>  // 'id' | 'name' | 'email'

// 2. 提取对象值的联合类型
type Values<T> = T[keyof T]
type UserValues = Values<User>  // string | number

// 3. 部分可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

type UserWithOptionalEmail = PartialBy<User, 'email'>
// { id: string; name: string; email?: string }

// 4. 深度只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

// 5. 记录类型
type UserRole = 'admin' | 'user' | 'guest'
type RolePermissions = Record<UserRole, string[]>

const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  user: ['read', 'write'],
  guest: ['read']
}

// 6. 排除类型
type Exclude<T, U> = T extends U ? never : T
type Admin = Exclude<UserRole, 'guest' | 'user'>  // 'admin'

// 7. 提取类型
type Extract<T, U> = T extends U ? T : never
type NotAdmin = Extract<UserRole, Exclude<UserRole, 'admin'>>  // 'user' | 'guest'

8. 最佳实践总结

// ✅ TypeScript 最佳实践清单

// 1. 优先使用 interface 定义数据结构
interface User {
  id: string
  name: string
}

// 2. 为函数参数和返回值添加类型
function getUser(id: string): Promise<User> {
  // ...
}

// 3. 避免使用 any,使用 unknown 然后类型保护
// ❌ const data: any
// ✅ const data: unknown
if (typeof data === 'object' && data !== null) {
  // 现在可以安全地使用 data
}

// 4. 使用字面量类型替代字符串常量
// ❌ status: string
// ✅ status: 'pending' | 'success' | 'error'

// 5. 充分利用泛型
const useData = <T>(url: string): Promise<T> => {
  // ...
}

// 6. 为 Vue 组件的 Props 和 Emits 定义类型
interface Props {
  title: string
  count?: number
}

type Emits = {
  'update:title': [title: string]
  'increment': []
}

总结

TypeScript 在 Vue 3 中的优势:

特性优势
类型安全编译时发现错误
自动补全IDE 提示更准确
可维护性代码意图更明确
重构安全改变代码有反馈
文档作用类型即文档

相关资源