📅 发布日期:2025年11月4日
🏷️ 标签:Vue3、TypeScript、类型推断、类型安全
⏱️ 阅读时长:约125分钟
💡 难度:⭐⭐ 进阶级

📖 前言

大家好,我是【代码小库】的创建者。这是 Vue3 实战系列 的第6篇文章。

Vue3 对 TypeScript 的支持是前所未有的好,使用 TS 可以让我们的代码更健壮、更易维护。今天我们将深入学习如何在 Vue3 项目中正确使用 TypeScript。

本文你将学到:

  • ✅ TypeScript 在 Vue3 项目中的完整配置
  • ✅ 组件 Props、Emits、Slots 的类型定义
  • ✅ Ref、Reactive、Computed 的类型推断
  • ✅ 组合式函数(Composables)的类型定义
  • ✅ API 接口类型定义和封装
  • ✅ Pinia Store 的类型安全
  • ✅ Vue Router 的类型扩展
  • ✅ 常见类型问题和解决方案
  • ✅ 实战案例:完整的类型安全项目

在这里插入图片描述

🔧 一、TypeScript 配置

1.1 tsconfig.json 完整配置

创建或修改 tsconfig.json

{
  "compilerOptions": {
    // ========== 基础配置 ==========
  
    // 目标 ECMAScript 版本
    // ES2020 包含了可选链、空值合并等现代特性
    "target": "ES2020",
  
    // 使用 ES2020 的库文件
    // DOM: 浏览器环境的类型定义
    // DOM.Iterable: 使 DOM 集合可迭代
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
  
    // 模块系统:ESNext 支持最新的 ES 模块语法
    "module": "ESNext",
  
    // 模块解析策略:bundler 适用于 Vite/Webpack 等打包工具
    "moduleResolution": "bundler",
  
    // ========== 严格模式(重要!启用所有严格检查)==========
  
    // 启用所有严格类型检查选项
    "strict": true,
  
    // 不允许隐式的 any 类型
    // 例如:function foo(x) { ... } 会报错,必须写成 function foo(x: any)
    "noImplicitAny": true,
  
    // 严格的 null 检查
    // 例如:let x: string = null 会报错
    "strictNullChecks": true,
  
    // 严格的函数类型检查
    "strictFunctionTypes": true,
  
    // 严格的 bind/call/apply 检查
    "strictBindCallApply": true,
  
    // 严格的属性初始化检查
    // 类的属性必须初始化或在构造函数中赋值
    "strictPropertyInitialization": true,
  
    // 不允许 this 的隐式 any
    "noImplicitThis": true,
  
    // 总是以严格模式检查每个文件
    "alwaysStrict": true,
  
    // ========== 额外的严格检查 ==========
  
    // 未使用的局部变量会报错
    "noUnusedLocals": true,
  
    // 未使用的函数参数会报错
    "noUnusedParameters": true,
  
    // 所有代码路径都必须有返回值
    "noImplicitReturns": true,
  
    // switch 语句的 fallthrough 检查
    "noFallthroughCasesInSwitch": true,
  
    // 未使用的标签会报错
    "allowUnusedLabels": false,
  
    // 不可达代码会报错
    "allowUnreachableCode": false,
  
    // ========== Vue 相关配置 ==========
  
    // JSX 语法支持:preserve 保留 JSX 语法,由其他工具处理
    "jsx": "preserve",
  
    // 允许导入 .ts 扩展名(Vite 会处理)
    "allowImportingTsExtensions": true,
  
    // 解析 JSON 模块
    "resolveJsonModule": true,
  
    // 独立模块:每个文件都作为单独的模块
    // Vite 需要这个选项
    "isolatedModules": true,
  
    // 不生成输出文件(由 Vite 处理编译)
    "noEmit": true,
  
    // 使用 defineComponent 的类的字段语义
    "useDefineForClassFields": true,
  
    // ========== 路径映射(重要!)==========
  
    // 基础路径:相对于 tsconfig.json
    "baseUrl": ".",
  
    // 路径别名配置(需要和 vite.config.ts 中的 alias 保持一致)
    "paths": {
      "@/*": ["src/*"],              // @/ 映射到 src/
      "@components/*": ["src/components/*"],
      "@views/*": ["src/views/*"],
      "@utils/*": ["src/utils/*"],
      "@api/*": ["src/api/*"],
      "@stores/*": ["src/stores/*"],
      "@types/*": ["src/types/*"]
    },
  
    // ========== 其他有用的选项 ==========
  
    // 跳过库文件的类型检查(加快编译速度)
    "skipLibCheck": true,
  
    // 允许从没有默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
  
    // 启用 ES 模块互操作性
    "esModuleInterop": true,
  
    // 强制一致的大小写敏感(避免跨平台问题)
    "forceConsistentCasingInFileNames": true
  },
  
  // ========== 包含的文件 ==========
  "include": [
    "src/**/*.ts",        // 所有 .ts 文件
    "src/**/*.d.ts",      // 所有类型声明文件
    "src/**/*.tsx",       // 所有 .tsx 文件
    "src/**/*.vue"        // 所有 .vue 文件
  ],
  
  // ========== 排除的文件 ==========
  "exclude": [
    "node_modules",       // 排除 node_modules
    "dist"                // 排除构建输出目录
  ],
  
  // ========== 引用其他 tsconfig 文件 ==========
  "references": [
    {
      "path": "./tsconfig.node.json"  // Vite 配置文件的 TS 配置
    }
  ]
}

1.2 tsconfig.node.json(Vite 配置文件专用)

{
  "compilerOptions": {
    // Node.js 环境
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "skipLibCheck": true
  },
  // 只包含 Vite 配置文件
  "include": ["vite.config.ts"]
}

1.3 env.d.ts(环境类型声明)

创建 src/env.d.ts

// ========== Vite 客户端类型 ==========
/// <reference types="vite/client" />

// ========== .vue 文件的类型声明 ==========
// 让 TypeScript 识别 .vue 文件
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

// ========== 环境变量类型定义 ==========
// 定义 import.meta.env 的类型
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string          // 应用标题
  readonly VITE_API_BASE_URL: string       // API 基础 URL
  readonly VITE_APP_ENV: 'development' | 'production' | 'staging'
  // 添加更多自定义环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

// ========== 图片资源类型 ==========
declare module '*.svg' {
  const content: string
  export default content
}

declare module '*.png' {
  const content: string
  export default content
}

declare module '*.jpg' {
  const content: string
  export default content
}

// ========== CSS Modules 类型 ==========
declare module '*.module.css' {
  const classes: { readonly [key: string]: string }
  export default classes
}

declare module '*.module.scss' {
  const classes: { readonly [key: string]: string }
  export default classes
}

📦 二、组件类型定义

2.1 Props 类型定义(三种方式)

方式一:运行时声明(基础)
<script setup lang="ts">
// ========== 1. 简单的 Props 类型 ==========
const props = defineProps<{
  // 必需的字符串
  title: string
  
  // 必需的数字
  count: number
  
  // 可选的布尔值
  disabled?: boolean
  
  // 数组类型
  tags: string[]
  
  // 对象类型
  user: {
    id: number
    name: string
    email: string
  }
}>()

// ========== 2. 使用接口定义(推荐)==========
interface User {
  id: number
  name: string
  email: string
  avatar?: string  // 可选属性
}

interface Props {
  title: string
  count: number
  disabled?: boolean
  tags: string[]
  user: User
  
  // 联合类型
  status: 'pending' | 'success' | 'error'
  
  // 函数类型
  onClick?: (event: MouseEvent) => void
  
  // 泛型类型
  items: Array<{ id: number; name: string }>
}

const props = defineProps<Props>()

// ========== 3. 使用 Props(会有完整的类型提示)==========
console.log(props.title)      // ✅ 类型:string
console.log(props.count)      // ✅ 类型:number
console.log(props.disabled)   // ✅ 类型:boolean | undefined

// props.title = 'new'  // ❌ 错误:props 是只读的
</script>
方式二:带默认值的 Props
<script setup lang="ts">
// ========== 使用 withDefaults 设置默认值 ==========
interface Props {
  title: string
  count?: number        // 可选
  disabled?: boolean    // 可选
  tags?: string[]       // 可选
}

// withDefaults:为可选 props 提供默认值
const props = withDefaults(defineProps<Props>(), {
  // 默认值的类型会被自动推断
  count: 0,
  disabled: false,
  
  // 对象/数组的默认值需要使用工厂函数
  tags: () => []
})

// ========== 复杂的默认值 ==========
interface ButtonProps {
  size?: 'small' | 'medium' | 'large'
  type?: 'primary' | 'secondary' | 'danger'
  loading?: boolean
  disabled?: boolean
}

const props = withDefaults(defineProps<ButtonProps>(), {
  size: 'medium',
  type: 'primary',
  loading: false,
  disabled: false
})
</script>
方式三:运行时 + 类型声明(最灵活)
<script setup lang="ts">
import type { PropType } from 'vue'

// ========== 复杂类型需要使用 PropType ==========
interface User {
  id: number
  name: string
  role: 'admin' | 'user'
}

const props = defineProps({
  // 简单类型
  title: {
    type: String,
    required: true,
    default: '默认标题'
  },
  
  // 数字类型 + 验证
  count: {
    type: Number,
    default: 0,
    validator: (value: number) => value >= 0  // 自定义验证
  },
  
  // 对象类型(使用 PropType)
  user: {
    type: Object as PropType<User>,  // 指定类型
    required: true
  },
  
  // 数组类型
  tags: {
    type: Array as PropType<string[]>,
    default: () => []  // 数组/对象必须使用工厂函数
  },
  
  // 函数类型
  onClick: {
    type: Function as PropType<(event: MouseEvent) => void>,
    required: false
  },
  
  // 联合类型
  status: {
    type: String as PropType<'pending' | 'success' | 'error'>,
    default: 'pending',
    // 验证器
    validator: (value: string) => {
      return ['pending', 'success', 'error'].includes(value)
    }
  }
})

// ========== 使用 Props(完整的类型推断)==========
console.log(props.user.name)  // ✅ 类型:string
console.log(props.tags[0])    // ✅ 类型:string | undefined
</script>

2.2 Emits 类型定义

<script setup lang="ts">
// ========== 方式一:简单的 Emits ==========
const emit = defineEmits<{
  // 事件名:参数类型
  'update:modelValue': [value: string]    // 单个参数
  'change': [id: number, name: string]    // 多个参数
  'submit': []                            // 无参数
}>()

// 使用
emit('update:modelValue', 'new value')
emit('change', 1, 'John')
emit('submit')

// ========== 方式二:使用类型别名(推荐)==========
type Emits = {
  // v-model 事件
  'update:modelValue': [value: string]
  
  // 表单提交事件
  'submit': [data: {
    username: string
    password: string
  }]
  
  // 删除事件
  'delete': [id: number]
  
  // 复杂事件
  'change': [event: {
    type: 'add' | 'remove' | 'update'
    payload: any
  }]
}

const emit = defineEmits<Emits>()

// ========== 方式三:运行时声明 + 验证 ==========
const emit = defineEmits({
  // 简单验证
  'update:modelValue': (value: string) => {
    return value.length > 0  // 返回 false 会在控制台警告
  },
  
  // 复杂验证
  'submit': (data: { username: string; password: string }) => {
    return data.username.length >= 3 && data.password.length >= 6
  },
  
  // 无验证
  'delete': null
})

// ========== 完整示例:表单组件 ==========
interface FormData {
  username: string
  email: string
  age: number
}

interface FormEmits {
  'submit': [data: FormData]                  // 提交表单
  'cancel': []                                // 取消
  'validate': [field: keyof FormData, valid: boolean]  // 字段验证
  'change': [field: keyof FormData, value: any]        // 字段改变
}

const emit = defineEmits<FormEmits>()

// 使用(会有完整的类型检查和自动补全)
function handleSubmit() {
  emit('submit', {
    username: 'john',
    email: 'john@example.com',
    age: 25
  })
}

function handleFieldChange(field: keyof FormData, value: any) {
  emit('change', field, value)
  emit('validate', field, value !== '')
}
</script>

2.3 defineExpose 类型定义

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

// ========== 组件内部的数据和方法 ==========
const count = ref(0)
const message = ref('Hello')

function increment() {
  count.value++
}

function reset() {
  count.value = 0
  message.value = 'Hello'
}

// ========== 暴露给父组件的类型定义 ==========
// 定义暴露的接口
interface ExposedMethods {
  count: number          // 暴露 count 的值(只读)
  increment: () => void  // 暴露 increment 方法
  reset: () => void      // 暴露 reset 方法
}

// 暴露给父组件(会有类型检查)
defineExpose<ExposedMethods>({
  get count() {
    return count.value  // 使用 getter 暴露响应式值
  },
  increment,
  reset
})
</script>

在父组件中使用:

<template>
  <ChildComponent ref="childRef" />
  <button @click="handleClick">调用子组件方法</button>
</template>

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

// ========== 获取子组件实例的类型 ==========
// 方式一:使用 InstanceType(推荐)
const childRef = ref<InstanceType<typeof ChildComponent>>()

// 方式二:手动定义类型
interface ChildComponentRef {
  count: number
  increment: () => void
  reset: () => void
}
const childRef2 = ref<ChildComponentRef>()

// ========== 使用(会有完整的类型提示)==========
function handleClick() {
  // ✅ 类型安全的方法调用
  childRef.value?.increment()
  
  // ✅ 访问暴露的属性
  console.log(childRef.value?.count)
  
  // ❌ 访问未暴露的属性会报错
  // childRef.value?.message  // 错误:Property 'message' does not exist
}
</script>

🎯 三、Reactive API 类型推断

3.1 ref 类型推断

import { ref, Ref } from 'vue'

// ========== 1. 自动类型推断 ==========
const count = ref(0)        // ✅ 类型自动推断为 Ref<number>
const message = ref('hi')   // ✅ 类型自动推断为 Ref<string>
const isActive = ref(false) // ✅ 类型自动推断为 Ref<boolean>

// ========== 2. 显式指定类型 ==========
const count2 = ref<number>(0)
const message2 = ref<string>('hi')

// ========== 3. 复杂类型需要显式指定 ==========
interface User {
  id: number
  name: string
  email: string
}

// ✅ 正确:显式指定类型
const user = ref<User>({
  id: 1,
  name: 'John',
  email: 'john@example.com'
})

// ✅ 访问属性(有完整的类型提示)
console.log(user.value.name)  // ✅ 类型:string

// ========== 4. 可能为 null 的类型 ==========
// 使用联合类型
const user2 = ref<User | null>(null)

// 使用前需要检查
if (user2.value) {
  console.log(user2.value.name)  // ✅ 类型安全
}

// 或使用可选链
console.log(user2.value?.name)

// ========== 5. 数组类型 ==========
const tags = ref<string[]>([])                    // 字符串数组
const users = ref<User[]>([])                     // 对象数组
const matrix = ref<number[][]>([[1, 2], [3, 4]]) // 二维数组

// ========== 6. 函数类型 ==========
const onClick = ref<((event: MouseEvent) => void) | null>(null)

// 赋值
onClick.value = (event: MouseEvent) => {
  console.log('Clicked', event)
}

// ========== 7. 泛型类型 ==========
// 创建通用的 ref 工厂函数
function useRef<T>(initialValue: T): Ref<T> {
  return ref(initialValue) as Ref<T>
}

const count3 = useRef(0)          // Ref<number>
const user3 = useRef<User | null>(null)  // Ref<User | null>

3.2 reactive 类型推断

import { reactive } from 'vue'

// ========== 1. 自动类型推断(推荐)==========
const state = reactive({
  count: 0,
  message: 'Hello',
  user: {
    id: 1,
    name: 'John'
  }
})

// ✅ 类型自动推断
console.log(state.count)      // number
console.log(state.message)    // string
console.log(state.user.name)  // string

// ========== 2. 使用接口定义类型(推荐)==========
interface State {
  count: number
  message: string
  user: {
    id: number
    name: string
  }
  tags: string[]
}

const state2: State = reactive({
  count: 0,
  message: 'Hello',
  user: {
    id: 1,
    name: 'John'
  },
  tags: []
})

// ========== 3. 复杂嵌套类型 ==========
interface User {
  id: number
  name: string
  profile: {
    avatar: string
    bio: string
    settings: {
      theme: 'light' | 'dark'
      language: string
    }
  }
}

interface AppState {
  user: User | null
  loading: boolean
  error: string | null
}

const appState: AppState = reactive({
  user: null,
  loading: false,
  error: null
})

// ========== 4. 注意:reactive 的类型陷阱 ==========
// ❌ 错误:重新赋值会失去响应式
let state3 = reactive({ count: 0 })
state3 = reactive({ count: 1 })  // ❌ 失去响应式!

// ✅ 正确:使用 ref 包装
const state4 = ref(reactive({ count: 0 }))
state4.value = reactive({ count: 1 })  // ✅ 保持响应式

3.3 computed 类型推断

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

// ========== 1. 自动类型推断 ==========
const count = ref(0)

// ✅ 类型自动推断为 ComputedRef<number>
const double = computed(() => count.value * 2)

// ========== 2. 显式指定返回类型 ==========
const double2 = computed<number>(() => {
  return count.value * 2
})

// ========== 3. 复杂类型推断 ==========
interface User {
  id: number
  name: string
  age: number
}

const users = ref<User[]>([
  { id: 1, name: 'John', age: 25 },
  { id: 2, name: 'Jane', age: 30 }
])

// ✅ 自动推断为 ComputedRef<User[]>
const adultUsers = computed(() => {
  return users.value.filter(user => user.age >= 18)
})

// ✅ 自动推断为 ComputedRef<string[]>
const userNames = computed(() => {
  return users.value.map(user => user.name)
})

// ========== 4. 可写的 computed ==========
const firstName = ref('John')
const lastName = ref('Doe')

// get 返回 string,set 接收 string
const fullName = computed<string>({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue: string) {
    const names = newValue.split(' ')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

// ========== 5. 泛型 computed ==========
function useComputed<T, R>(
  source: Ref<T>,
  transform: (value: T) => R
): ComputedRef<R> {
  return computed(() => transform(source.value))
}

const count2 = ref(10)
// ✅ 类型自动推断为 ComputedRef<string>
const countText = useComputed(count2, (value) => `Count: ${value}`)

3.4 watch 类型推断

import { ref, watch, WatchSource, WatchCallback } from 'vue'

// ========== 1. 监听单个 ref ==========
const count = ref(0)

// 回调函数的参数类型会自动推断
watch(count, (newValue, oldValue) => {
  // newValue: number
  // oldValue: number
  console.log(`${oldValue} 变成 ${newValue}`)
})

// ========== 2. 监听多个源 ==========
const firstName = ref('John')
const lastName = ref('Doe')

watch(
  [firstName, lastName],  // 源数组
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    // 类型都会自动推断
    console.log(`${oldFirst} ${oldLast} 变成 ${newFirst} ${newLast}`)
  }
)

// ========== 3. 监听 reactive 对象的属性 ==========
interface User {
  name: string
  age: number
}

const user = reactive<User>({
  name: 'John',
  age: 25
})

// 使用 getter 函数
watch(
  () => user.age,  // getter 函数
  (newAge, oldAge) => {
    // newAge: number
    // oldAge: number
    console.log(`年龄从 ${oldAge} 变成 ${newAge}`)
  }
)

// ========== 4. 监听复杂类型 ==========
interface Todo {
  id: number
  text: string
  completed: boolean
}

const todos = ref<Todo[]>([])

watch(
  todos,
  (newTodos, oldTodos) => {
    // newTodos: Todo[]
    // oldTodos: Todo[]
    console.log(`Todo 数量:${newTodos.length}`)
  },
  { deep: true }  // 深度监听
)

// ========== 5. 自定义 watch 类型 ==========
function useWatch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>
) {
  return watch(source, callback)
}

const message = ref('Hello')
useWatch(message, (newValue) => {
  // newValue: string(自动推断)
  console.log(newValue)
})

🔌 四、Composables 类型定义

4.1 基础 Composable

// composables/useCounter.ts
import { ref, computed, Ref, ComputedRef } from 'vue'

/**
 * 返回值类型定义(重要!)
 * 明确定义返回值类型,让使用者知道有哪些属性和方法
 */
interface UseCounterReturn {
  count: Ref<number>                    // 响应式状态
  double: ComputedRef<number>           // 计算属性
  increment: () => void                 // 方法
  decrement: () => void
  reset: () => void
  set: (value: number) => void
}

/**
 * 计数器 Composable
 * @param initialValue 初始值(可选,默认为 0)
 * @returns 计数器的状态和方法
 */
export function useCounter(initialValue = 0): UseCounterReturn {
  // ========== 状态 ==========
  const count = ref(initialValue)
  
  // ========== 计算属性 ==========
  const double = computed(() => count.value * 2)
  
  // ========== 方法 ==========
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  function set(value: number) {
    count.value = value
  }
  
  // ========== 返回(类型安全)==========
  return {
    count,
    double,
    increment,
    decrement,
    reset,
    set
  }
}

// ========== 使用示例 ==========
// 在组件中使用
const { count, double, increment } = useCounter(10)

console.log(count.value)    // 10
console.log(double.value)   // 20
increment()
console.log(count.value)    // 11

4.2 泛型 Composable

// composables/useFetch.ts
import { ref, Ref } from 'vue'

/**
 * 返回值类型(泛型)
 * T: 数据类型
 */
interface UseFetchReturn<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  execute: () => Promise<void>
  refresh: () => Promise<void>
}

/**
 * 通用的数据请求 Composable
 * @param url API 地址
 * @returns 请求状态和方法
 * 
 * @example
 * interface User { id: number; name: string }
 * const { data, loading, execute } = useFetch<User>('/api/user/1')
 */
export function useFetch<T = any>(url: string): UseFetchReturn<T> {
  // ========== 状态 ==========
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  // ========== 请求方法 ==========
  async function execute() {
    loading.value = true
    error.value = null
  
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`)
      }
      // 这里需要类型断言,因为 fetch 返回的是 any
      data.value = (await response.json()) as T
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }
  
  async function refresh() {
    await execute()
  }
  
  return {
    data,
    loading,
    error,
    execute,
    refresh
  }
}

// ========== 使用示例 ==========
interface User {
  id: number
  name: string
  email: string
}

// ✅ 指定泛型类型,获得完整的类型推断
const { data, loading, error, execute } = useFetch<User>('/api/user/1')

// data 的类型是 Ref<User | null>
if (data.value) {
  console.log(data.value.name)   // ✅ 类型:string
  console.log(data.value.email)  // ✅ 类型:string
}

4.3 高级 Composable(带配置项)

// composables/useAsync.ts
import { ref, Ref, UnwrapRef } from 'vue'

/**
 * 配置选项类型
 */
interface UseAsyncOptions {
  immediate?: boolean           // 是否立即执行
  onSuccess?: (data: any) => void   // 成功回调
  onError?: (error: Error) => void  // 错误回调
  resetOnExecute?: boolean      // 执行时是否重置数据
}

/**
 * 返回值类型(泛型)
 * T: 异步函数的返回值类型
 * P: 异步函数的参数类型数组
 */
interface UseAsyncReturn<T, P extends any[]> {
  data: Ref<UnwrapRef<T> | null>
  loading: Ref<boolean>
  error: Ref<Error | null>
  execute: (...args: P) => Promise<T>
}

/**
 * 异步操作 Composable
 * @param asyncFunction 异步函数
 * @param options 配置选项
 * 
 * @example
 * const fetchUser = (id: number) => fetch(`/api/users/${id}`).then(r => r.json())
 * const { data, loading, execute } = useAsync(fetchUser, { immediate: false })
 * await execute(1)  // 执行请求
 */
export function useAsync<T, P extends any[] = []>(
  asyncFunction: (...args: P) => Promise<T>,
  options: UseAsyncOptions = {}
): UseAsyncReturn<T, P> {
  const {
    immediate = false,
    onSuccess,
    onError,
    resetOnExecute = true
  } = options
  
  // ========== 状态 ==========
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  // ========== 执行方法 ==========
  async function execute(...args: P): Promise<T> {
    // 重置状态
    if (resetOnExecute) {
      data.value = null
      error.value = null
    }
  
    loading.value = true
  
    try {
      const result = await asyncFunction(...args)
      data.value = result as UnwrapRef<T>
    
      // 成功回调
      onSuccess?.(result)
    
      return result
    } catch (err) {
      const errorObj = err as Error
      error.value = errorObj
    
      // 错误回调
      onError?.(errorObj)
    
      throw err
    } finally {
      loading.value = false
    }
  }
  
  // ========== 立即执行 ==========
  if (immediate) {
    execute(...([] as unknown as P))
  }
  
  return {
    data,
    loading,
    error,
    execute
  }
}

// ========== 使用示例 ==========
interface User {
  id: number
  name: string
}

// 定义异步函数
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// 使用 Composable
const { data, loading, error, execute } = useAsync(fetchUser, {
  immediate: false,
  onSuccess: (user) => {
    console.log('获取用户成功:', user.name)
  },
  onError: (err) => {
    console.error('获取用户失败:', err.message)
  }
})

// 执行请求(参数类型会被检查)
await execute(1)  // ✅ 正确
// await execute('1')  // ❌ 错误:参数类型不匹配

// 访问数据(类型安全)
if (data.value) {
  console.log(data.value.name)  // ✅ 类型:string
}

🌐 五、API 接口类型定义

5.1 响应数据类型定义

// types/api.ts

/**
 * API 通用响应格式
 * T: 业务数据类型
 */
export interface ApiResponse<T = any> {
  code: number              // 状态码(200 成功,其他失败)
  message: string          // 提示信息
  data: T                  // 业务数据
  timestamp?: number       // 时间戳
}

/**
 * 分页响应格式
 * T: 列表项类型
 */
export interface PageResponse<T> {
  list: T[]                // 数据列表
  total: number            // 总数
  page: number             // 当前页码
  pageSize: number         // 每页数量
  hasMore: boolean         // 是否还有更多
}

/**
 * 分页请求参数
 */
export interface PageParams {
  page: number
  pageSize: number
  keyword?: string         // 搜索关键词
  sortBy?: string          // 排序字段
  sortOrder?: 'asc' | 'desc'
}

5.2 业务数据类型定义

// types/user.ts

/**
 * 用户信息
 */
export interface User {
  id: number
  username: string
  email: string
  avatar?: string
  role: 'admin' | 'user' | 'vip'
  status: 'active' | 'inactive' | 'banned'
  createdAt: string
  updatedAt: string
}

/**
 * 登录请求参数
 */
export interface LoginParams {
  username: string
  password: string
  remember?: boolean
}

/**
 * 登录响应数据
 */
export interface LoginResponse {
  user: User
  token: string
  refreshToken: string
  expiresIn: number
}

/**
 * 更新用户信息参数
 */
export interface UpdateUserParams {
  avatar?: string
  email?: string
  phone?: string
  bio?: string
}

5.3 API 请求封装

// api/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { ApiResponse } from '@/types/api'

/**
 * 创建 Axios 实例
 */
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 15000,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

/**
 * 请求拦截器
 */
service.interceptors.request.use(
  (config) => {
    // 添加 token
    const token = localStorage.getItem('token')
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

/**
 * 响应拦截器
 */
service.interceptors.response.use(
  (response: AxiosResponse<ApiResponse>) => {
    const res = response.data
  
    // 根据业务状态码判断
    if (res.code !== 200) {
      // 处理业务错误
      return Promise.reject(new Error(res.message || '请求失败'))
    }
  
    return res
  },
  (error) => {
    return Promise.reject(error)
  }
)

/**
 * 通用请求方法(带类型推断)
 * @param config Axios 配置
 * @returns Promise<T>
 */
export function request<T = any>(
  config: AxiosRequestConfig
): Promise<ApiResponse<T>> {
  return service.request<any, ApiResponse<T>>(config)
}

/**
 * GET 请求
 */
export function get<T = any>(
  url: string,
  params?: any,
  config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
  return request<T>({
    url,
    method: 'GET',
    params,
    ...config
  })
}

/**
 * POST 请求
 */
export function post<T = any>(
  url: string,
  data?: any,
  config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
  return request<T>({
    url,
    method: 'POST',
    data,
    ...config
  })
}

/**
 * PUT 请求
 */
export function put<T = any>(
  url: string,
  data?: any,
  config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
  return request<T>({
    url,
    method: 'PUT',
    data,
    ...config
  })
}

/**
 * DELETE 请求
 */
export function del<T = any>(
  url: string,
  config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
  return request<T>({
    url,
    method: 'DELETE',
    ...config
  })
}

export default service

5.4 具体API定义

// api/user.ts
import { get, post, put } from './request'
import type {
  User,
  LoginParams,
  LoginResponse,
  UpdateUserParams
} from '@/types/user'
import type { ApiResponse, PageResponse, PageParams } from '@/types/api'

/**
 * 用户登录
 * @param data 登录参数
 * @returns Promise<ApiResponse<LoginResponse>>
 */
export function loginApi(data: LoginParams) {
  return post<LoginResponse>('/auth/login', data)
}

/**
 * 获取当前用户信息
 * @returns Promise<ApiResponse<User>>
 */
export function getCurrentUserApi() {
  return get<User>('/user/current')
}

/**
 * 更新用户信息
 * @param data 更新参数
 * @returns Promise<ApiResponse<User>>
 */
export function updateUserApi(data: UpdateUserParams) {
  return put<User>('/user/profile', data)
}

/**
 * 获取用户列表(分页)
 * @param params 分页参数
 * @returns Promise<ApiResponse<PageResponse<User>>>
 */
export function getUserListApi(params: PageParams) {
  return get<PageResponse<User>>('/users', params)
}

/**
 * 获取用户详情
 * @param id 用户 ID
 * @returns Promise<ApiResponse<User>>
 */
export function getUserByIdApi(id: number) {
  return get<User>(`/users/${id}`)
}

5.5 在组件中使用(类型安全)

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { loginApi, getCurrentUserApi, getUserListApi } from '@/api/user'
import type { User } from '@/types/user'
import type { PageParams } from '@/types/api'

// ========== 登录 ==========
async function handleLogin() {
  try {
    // ✅ 参数类型会被检查
    const response = await loginApi({
      username: 'admin',
      password: '123456',
      remember: true
    })
  
    // ✅ response.data 的类型是 LoginResponse
    console.log('用户信息:', response.data.user.username)
    console.log('Token:', response.data.token)
  
  } catch (error) {
    console.error('登录失败:', error)
  }
}

// ========== 获取用户信息 ==========
const currentUser = ref<User | null>(null)

async function fetchCurrentUser() {
  try {
    const response = await getCurrentUserApi()
  
    // ✅ response.data 的类型是 User
    currentUser.value = response.data
  
    // ✅ 有完整的类型提示
    console.log(currentUser.value?.username)
    console.log(currentUser.value?.email)
  
  } catch (error) {
    console.error('获取用户信息失败:', error)
  }
}

// ========== 获取用户列表 ==========
const users = ref<User[]>([])
const total = ref(0)

async function fetchUsers() {
  try {
    // ✅ 参数类型会被检查
    const params: PageParams = {
      page: 1,
      pageSize: 10,
      keyword: 'john',
      sortBy: 'createdAt',
      sortOrder: 'desc'
    }
  
    const response = await getUserListApi(params)
  
    // ✅ response.data 的类型是 PageResponse<User>
    users.value = response.data.list
    total.value = response.data.total
  
    // ✅ 遍历时有完整的类型提示
    users.value.forEach(user => {
      console.log(user.username)  // ✅ 类型:string
      console.log(user.email)     // ✅ 类型:string
    })
  
  } catch (error) {
    console.error('获取用户列表失败:', error)
  }
}

onMounted(() => {
  fetchCurrentUser()
  fetchUsers()
})
</script>

🛡️ 六、Pinia Store 类型定义

6.1 选项式 Store

// stores/user.ts
import { defineStore } from 'pinia'
import { loginApi, getCurrentUserApi } from '@/api/user'
import type { User, LoginParams } from '@/types/user'

/**
 * State 类型定义(重要!)
 */
interface UserState {
  user: User | null
  token: string | null
  loading: boolean
  error: string | null
}

/**
 * 用户 Store
 */
export const useUserStore = defineStore('user', {
  // ========== State ==========
  state: (): UserState => ({
    user: null,
    token: localStorage.getItem('token'),
    loading: false,
    error: null
  }),
  
  // ========== Getters ==========
  getters: {
    // 自动推断返回类型为 boolean
    isLoggedIn: (state): boolean => {
      return !!state.token && !!state.user
    },
  
    // 显式指定返回类型
    username(): string {
      return this.user?.username || '游客'
    },
  
    // 带参数的 getter(返回函数)
    hasRole: (state) => {
      return (role: string): boolean => {
        return state.user?.role === role
      }
    }
  },
  
  // ========== Actions ==========
  actions: {
    /**
     * 登录
     * @param params 登录参数
     */
    async login(params: LoginParams): Promise<void> {
      this.loading = true
      this.error = null
    
      try {
        const response = await loginApi(params)
      
        // ✅ response.data 有完整的类型
        this.user = response.data.user
        this.token = response.data.token
      
        // 保存 token
        localStorage.setItem('token', response.data.token)
      
      } catch (error: any) {
        this.error = error.message
        throw error
      } finally {
        this.loading = false
      }
    },
  
    /**
     * 获取当前用户信息
     */
    async fetchCurrentUser(): Promise<User | null> {
      this.loading = true
    
      try {
        const response = await getCurrentUserApi()
        this.user = response.data
        return response.data
      } catch (error: any) {
        this.error = error.message
        return null
      } finally {
        this.loading = false
      }
    },
  
    /**
     * 登出
     */
    logout(): void {
      this.user = null
      this.token = null
      localStorage.removeItem('token')
    }
  }
})

6.2 组合式 Store(Setup Store)

// stores/cart.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

/**
 * 购物车商品类型
 */
export interface CartItem {
  id: string
  productId: string
  name: string
  price: number
  quantity: number
  image: string
}

/**
 * 购物车 Store(组合式)
 */
export const useCartStore = defineStore('cart', () => {
  // ========== State ==========
  const items = ref<CartItem[]>([])
  
  // ========== Getters ==========
  const totalItems = computed(() => {
    return items.value.reduce((sum, item) => sum + item.quantity, 0)
  })
  
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => {
      return sum + item.price * item.quantity
    }, 0)
  })
  
  const isEmpty = computed(() => items.value.length === 0)
  
  // ========== Actions ==========
  
  /**
   * 添加商品到购物车
   * @param product 商品信息(不含 quantity)
   */
  function addItem(product: Omit<CartItem, 'quantity'>) {
    const existingItem = items.value.find(
      item => item.productId === product.productId
    )
  
    if (existingItem) {
      // 商品已存在,增加数量
      existingItem.quantity++
    } else {
      // 新商品,添加到购物车
      items.value.push({
        ...product,
        quantity: 1
      })
    }
  }
  
  /**
   * 移除商品
   * @param productId 商品 ID
   */
  function removeItem(productId: string) {
    const index = items.value.findIndex(
      item => item.productId === productId
    )
    if (index !== -1) {
      items.value.splice(index, 1)
    }
  }
  
  /**
   * 更新商品数量
   * @param productId 商品 ID
   * @param quantity 新数量
   */
  function updateQuantity(productId: string, quantity: number) {
    const item = items.value.find(item => item.productId === productId)
    if (item) {
      if (quantity <= 0) {
        removeItem(productId)
      } else {
        item.quantity = quantity
      }
    }
  }
  
  /**
   * 清空购物车
   */
  function clearCart() {
    items.value = []
  }
  
  // ========== 返回(类型会自动推断)==========
  return {
    // State
    items,
    // Getters
    totalItems,
    totalPrice,
    isEmpty,
    // Actions
    addItem,
    removeItem,
    updateQuantity,
    clearCart
  }
})

6.3 在组件中使用 Store(类型安全)

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

// ========== 用户 Store ==========
const userStore = useUserStore()

// 解构 state 和 getters(保持响应式)
const { user, loading, isLoggedIn, username } = storeToRefs(userStore)

// 解构 actions(不需要 storeToRefs)
const { login, logout } = userStore

// ✅ 使用(有完整的类型提示)
console.log(user.value?.email)        // string | undefined
console.log(isLoggedIn.value)         // boolean
console.log(username.value)           // string

// ✅ 调用 action(参数类型会被检查)
await login({
  username: 'admin',
  password: '123456',
  remember: true
})

// ========== 购物车 Store ==========
const cartStore = useCartStore()
const { items, totalItems, totalPrice } = storeToRefs(cartStore)

// ✅ 添加商品(参数类型会被检查)
cartStore.addItem({
  id: '1',
  productId: 'prod-1',
  name: 'iPhone 15',
  price: 5999,
  image: '/images/iphone15.jpg'
})

// ✅ 遍历购物车(类型安全)
items.value.forEach(item => {
  console.log(item.name)      // string
  console.log(item.price)     // number
  console.log(item.quantity)  // number
})
</script>

🚦 七、Vue Router 类型扩展

7.1 扩展路由元信息类型

// types/router.d.ts

/**
 * 扩展 Vue Router 的类型
 */
declare module 'vue-router' {
  interface RouteMeta {
    // 页面标题
    title?: string
  
    // 是否需要登录
    requiresAuth?: boolean
  
    // 允许访问的角色
    roles?: Array<'admin' | 'user' | 'vip'>
  
    // 是否需要 VIP
    requiresVIP?: boolean
  
    // 图标
    icon?: string
  
    // 是否在菜单中隐藏
    hidden?: boolean
  
    // 是否缓存组件
    keepAlive?: boolean
  
    // 面包屑配置
    breadcrumb?: {
      label: string
      path?: string
    }[]
  
    // 权限码
    permissions?: string[]
  }
}

export {}

7.2 路由配置(类型安全)

// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

/**
 * 路由配置(现在 meta 有完整的类型提示)
 */
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: {
      title: '首页',
      requiresAuth: false,  // ✅ 有类型提示
      keepAlive: true
    }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/layouts/AdminLayout.vue'),
    meta: {
      title: '管理后台',
      requiresAuth: true,
      roles: ['admin'],     // ✅ 只能是 'admin' | 'user' | 'vip'
      icon: 'dashboard'
    },
    children: [
      {
        path: 'users',
        name: 'AdminUsers',
        component: () => import('@/views/admin/Users.vue'),
        meta: {
          title: '用户管理',
          requiresAuth: true,
          roles: ['admin'],
          permissions: ['user:view', 'user:edit']
        }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

7.3 在守卫中使用(类型安全)

// router/index.ts
import { useUserStore } from '@/stores/user'

router.beforeEach((to, from, next) => {
  // ✅ to.meta 现在有完整的类型
  const title = to.meta.title
  if (title) {
    document.title = title
  }
  
  // ✅ 检查是否需要登录
  if (to.meta.requiresAuth) {
    const userStore = useUserStore()
  
    if (!userStore.isLoggedIn) {
      next({ path: '/login', query: { redirect: to.fullPath } })
      return
    }
  
    // ✅ 检查角色权限
    const roles = to.meta.roles
    if (roles && roles.length > 0) {
      const userRole = userStore.user?.role
      if (!userRole || !roles.includes(userRole)) {
        next({ name: 'Forbidden' })
        return
      }
    }
  
    // ✅ 检查 VIP 权限
    if (to.meta.requiresVIP) {
      if (userStore.user?.role !== 'vip') {
        next({ path: '/upgrade' })
        return
      }
    }
  }
  
  next()
})

❓ 八、常见类型问题和解决方案

8.1 问题:ref 类型推断不准确

// ❌ 问题:类型被推断为 Ref<never[]>
const list = ref([])

list.value.push({ id: 1, name: 'John' })  // ❌ 错误!

// ✅ 解决方案:显式指定类型
interface Item {
  id: number
  name: string
}

const list = ref<Item[]>([])
list.value.push({ id: 1, name: 'John' })  // ✅ 正确

8.2 问题:reactive 解构失去响应式

// ❌ 问题:解构后失去响应式
const state = reactive({ count: 0 })
const { count } = state

count++  // ❌ 不会触发更新!

// ✅ 解决方案1:使用 toRefs
import { toRefs } from 'vue'

const state = reactive({ count: 0 })
const { count } = toRefs(state)

count.value++  // ✅ 正确

// ✅ 解决方案2:使用 computed
const count = computed({
  get: () => state.count,
  set: (val) => { state.count = val }
})

8.3 问题:组件实例类型

<template>
  <ChildComponent ref="childRef" />
</template>

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

// ❌ 错误:类型是 Ref<any>
const childRef = ref()

// ✅ 正确:使用 InstanceType
const childRef = ref<InstanceType<typeof ChildComponent>>()

// 使用(有完整的类型提示)
childRef.value?.someMethod()
</script>

8.4 问题:泛型组件Props

<!-- GenericList.vue -->
<script setup lang="ts" generic="T">
/**
 * 泛型组件:可以接收任意类型的列表数据
 */
defineProps<{
  items: T[]
  renderItem: (item: T) => string
}>()
</script>

<template>
  <div>
    <div v-for="(item, index) in items" :key="index">
      {{ renderItem(item) }}
    </div>
  </div>
</template>

<!-- 使用 -->
<script setup lang="ts">
interface User {
  id: number
  name: string
}

const users: User[] = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' }
]

const renderUser = (user: User) => user.name
</script>

<template>
  <GenericList :items="users" :renderItem="renderUser" />
</template>

🎉 九、总结

本文详细讲解了 Vue3 + TypeScript 的完整配置和使用:

  1. TypeScript 配置 - tsconfig.json 完整配置详解
  2. 组件类型 - Props、Emits、Expose 的类型定义
  3. Reactive API - ref、reactive、computed、watch 的类型推断
  4. Composables - 基础、泛型、高级 Composable 的类型定义
  5. API 封装 - 请求封装和接口类型定义
  6. Pinia Store - 选项式和组合式 Store 的类型定义
  7. Vue Router - 路由元信息类型扩展
  8. 常见问题 - 类型问题和解决方案

下一篇预告:

在下一篇文章中,我将讲解 Vue3 性能优化:从40FPS到60FPS,包括:

  • 虚拟列表优化
  • 组件懒加载
  • 计算属性缓存
  • 防抖和节流
  • 打包优化

📦 获取完整源码

本文的完整代码示例(类型定义、API封装、Store配置),已经整理好了。

🔍 百度/Google 搜索:代码小库

在网站首页搜索「Vue3 TypeScript 配置」即可找到:

  • ✅ 完整的 TypeScript 配置文件
  • ✅ 所有类型定义文件
  • ✅ API 封装完整代码
  • ✅ 视频讲解

📢 关于作者

前端开发者,【代码小库】创建者,专注 Vue/React/Nuxt 实战教程。

🎯 我的公众号:代码小库

关注后回复关键词获取资源:

  • 回复 vue3 → Vue3 实战源码包
  • 回复 typescript → TS 完整配置文件
  • 回复 进群 → 加入技术交流群

💡 本文是【Vue3 实战系列】第 6 篇,完整系列 30 篇,持续更新中…

📚 往期回顾:

  • ✅ 第1-5篇:基础配置、Composition API、响应式原理、Pinia、Vue Router

🔜 下期预告:

  • ⏳ 第7篇:Vue3 性能优化:从40FPS到60FPS

❤️ 觉得有帮助? 点赞👍 收藏⭐ 关注👀
💬 有疑问? 欢迎评论区留言,我会认真回复每一条!


#Vue3 #TypeScript #类型安全 #类型推断 #代码小库

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐