(第六章) Vue3 + TypeScript 项目配置完全指南【类型安全】
·
📅 发布日期: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 的完整配置和使用:
- ✅ TypeScript 配置 - tsconfig.json 完整配置详解
- ✅ 组件类型 - Props、Emits、Expose 的类型定义
- ✅ Reactive API - ref、reactive、computed、watch 的类型推断
- ✅ Composables - 基础、泛型、高级 Composable 的类型定义
- ✅ API 封装 - 请求封装和接口类型定义
- ✅ Pinia Store - 选项式和组合式 Store 的类型定义
- ✅ Vue Router - 路由元信息类型扩展
- ✅ 常见问题 - 类型问题和解决方案
下一篇预告:
在下一篇文章中,我将讲解 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 #类型安全 #类型推断 #代码小库
更多推荐



所有评论(0)