【Vue3 实战系列·第 06 篇】TypeScript + Vue3:类型安全·泛型组件·类型推导——让错误在编译时暴露
【Vue3 实战系列·第 06 篇】TypeScript + Vue3:类型安全·泛型组件·类型推导——让错误在编译时暴露
系列回顾:第 01 篇组合式 API,第 02 篇组件通信,第 03 篇 Pinia 状态管理,第 04 篇 Vue Router 路由,第 05 篇性能优化。本篇是 Vue3 实战系列的收官之作:TypeScript + Vue3。TypeScript 不是"锦上添花",而是"雪中送炭"——它让 Props 传错类型在编译时报错而不是运行时崩溃,让 Emit 事件参数自动检查,让 Composable 返回值类型自动推导,让泛型组件一次编写适配多种数据类型。Vue3 对 TypeScript 的支持是"一等公民"级别的——
<script setup lang="ts">让类型声明更简洁,defineProps<T>()泛型声明比运行时声明更强大,defineEmits<T>()让事件类型安全,Vue 3.3+ 的generic="T"让泛型组件成为现实,InstanceType<typeof Comp>让 ref 模板类型精确推导。但 TypeScript 也有陷阱——any是万恶之源、as类型断言是"掩耳盗铃"、ref 不写泛型推导为any、Props 不完整声明导致类型丢失。今天,我们从类型安全基础、泛型组件与 Composable 到常见陷阱与最佳实践,彻底掌握 TypeScript + Vue3。
📑 文章目录
🛡️ 一、类型安全基础:Props·Emits·Ref

1.1 defineProps:泛型声明 > 运行时声明
Vue3 的 <script setup lang="ts"> 提供了两种 Props 声明方式:运行时声明(Vue2 风格,基于对象)和泛型声明(TypeScript 风格,基于类型)。泛型声明是官方推荐的方式。
运行时声明(不推荐):
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
items: { type: Array, required: true }, // ❌ 元素类型丢失!
})
泛型声明(推荐):
const props = defineProps<{
title: string
count?: number // 可选 = 非必填
items: string[] // ✅ 元素类型保留!
callback?: (id: number) => void // ✅ 函数类型!
status?: 'active' | 'inactive' // ✅ 联合类型!
}>()
// 带默认值
const props = withDefaults(defineProps<{
title: string
count?: number
items?: string[]
}>(), {
count: 0,
items: () => [], // 对象/数组默认值必须用工厂函数
})
泛型声明的优势:类型完整(复杂类型、联合类型、函数类型都支持)、IDE 自动补全(输入 props. 立即提示所有属性和类型)、导入类型(可以从其他文件导入 interface 作为 Props 类型)、代码更少(不需要 type + required + default 三行,一行搞定)。
1.2 defineEmits:事件类型安全
// 泛型声明(推荐)
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', item: Item): void
(e: 'change', value: string, old: string): void
}>()
// 调用时类型自动检查
emit('update', 42) // ✅ OK
emit('update', 'hello') // ❌ TS Error: string 不能赋给 number
emit('change', 'a', 'b') // ✅ OK
emit('change', 'a') // ❌ TS Error: 缺少参数
defineEmits 泛型声明让每个事件的参数类型都精确约束——调用 emit 时,参数类型和数量都会自动检查,传错类型或漏传参数都会在编译时报错。
1.3 ref 模板类型:InstanceType
当父组件通过 ref 获取子组件实例时,需要用 InstanceType<typeof Comp> 约束类型,才能获得子组件 expose 的方法提示:
<!-- 父组件 -->
<script setup lang="ts">
import ChildComp from './ChildComp.vue'
// ✅ 精确类型——自动推导 expose 的方法
const childRef = ref<InstanceType<typeof ChildComp>>()
// 调用子组件方法有类型提示
childRef.value?.focus() // ✅ 有提示
childRef.value?.reset() // ✅ 有提示
childRef.value?.nonExist() // ❌ TS Error
</script>
<template>
<ChildComp ref="childRef" />
</template>
<!-- 子组件 ChildComp.vue -->
<script setup lang="ts">
const focus = () => { /* ... */ }
const reset = () => { /* ... */ }
defineExpose({ focus, reset })
</script>
🧬 二、泛型组件与 Composable:一次编写多种类型

2.1 泛型组件:generic=“T”(Vue 3.3+)
Vue 3.3 引入了 <script setup lang="ts" generic="T"> 语法,让 SFC 组件支持泛型。这是 Vue3+TS 最重要的特性之一——一个泛型组件可以适配多种数据类型,不需要为 User[]、Product[]、Order[] 各写一个列表组件。
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: number }">
const props = defineProps<{
items: T[]
labelKey: keyof T // ✅ 只能是 T 的属性名
valueKey?: keyof T
selectable?: boolean
}>()
const emit = defineEmits<{
(e: 'select', item: T): void
(e: 'delete', id: number): void
}>()
const selectedId = ref<number | null>(null)
const handleSelect = (item: T) => {
selectedId.value = item.id
emit('select', item)
}
</script>
<template>
<div class="generic-list">
<div
v-for="item in items"
:key="item.id"
@click="handleSelect(item)"
>
{{ item[labelKey] }}
</div>
</div>
</template>
使用泛型组件——类型自动推导:
<script setup lang="ts">
import GenericList from './GenericList.vue'
interface User { id: number; name: string; email: string }
interface Product { id: number; title: string; price: number }
const users: User[] = [...]
const products: Product[] = [...]
// ✅ T 自动推导为 User
// labelKey 只能是 'id' | 'name' | 'email'
<GenericList :items="users" label-key="name" @select="onUserSelect" />
// ✅ T 自动推导为 Product
// labelKey 只能是 'id' | 'title' | 'price'
<GenericList :items="products" label-key="title" @select="onProductSelect" />
// ❌ TS Error: 'phone' 不是 User 的属性
<GenericList :items="users" label-key="phone" />
</script>
泛型组件的价值:写一个 GenericList 代替 UserList + ProductList + OrderList——DRY 原则的终极实现。keyof T 约束让 labelKey 只能是 T 的属性名,传错编译报错。
2.2 泛型 Composable:function()
泛型 Composable 是类型安全的逻辑复用——useFetch<T>() 自动推导 data 类型,useLocalStorage<T>() 自动推导存储值类型。
// composables/useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<string | null>
loading: Ref<boolean>
refetch: () => Promise<void>
}
export async function useFetch<T>(
url: string,
options?: RequestInit
): Promise<UseFetchReturn<T>> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const loading = ref(true)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const res = await fetch(url, options)
data.value = await res.json() as T // ✅ 类型安全
} catch (e) {
error.value = (e as Error).message
} finally {
loading.value = false
}
}
await fetchData()
return { data, error, loading, refetch: fetchData }
}
使用泛型 Composable——类型自动推导:
// ✅ data 类型自动推导为 User[]
const { data: users } = await useFetch<User[]>('/api/users')
// users.value[0].name ✅ 有提示
// users.value[0].age ✅ 有提示
// ✅ data 类型自动推导为 Product
const { data: product } = await useFetch<Product>('/api/products/1')
// product.value?.title ✅ 有提示
// product.value?.price ✅ 有提示
// composables/useLocalStorage.ts
export function useLocalStorage<T>(
key: string,
defaultValue: T
): { data: Ref<T> } {
const data = ref<T>(defaultValue) as Ref<T>
const stored = localStorage.getItem(key)
if (stored) {
data.value = JSON.parse(stored) as T
}
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
return { data }
}
// 使用
const { data: theme } = useLocalStorage<string>('theme', 'light')
const { data: settings } = useLocalStorage<AppSettings>('settings', defaults)
⚠️ 三、常见陷阱与最佳实践:远离 any

3.1 五大 TypeScript 陷阱
陷阱 1:使用 any——any 是类型安全的"黑洞",一旦引入,所有类型检查都失效。
// ❌ any——类型黑洞
const data: any = await fetch('/api/users')
data.whatever() // 不报错,但运行时崩溃
// ✅ 用 unknown 代替 any
const data: unknown = await fetch('/api/users')
// data.whatever() // TS Error: unknown 上没有方法
if (typeof data === 'object' && data !== null) {
// 类型收窄后安全使用
}
陷阱 2:滥用 as 类型断言——as 是"掩耳盗铃",告诉编译器"我知道类型",但实际可能不是。
// ❌ as 断言——掩耳盗铃
const user = data as User // 如果 data 不是 User,运行时崩溃
// ✅ 用类型守卫
function isUser(data: unknown): data is User {
return typeof data === 'object'
&& data !== null
&& 'name' in data
&& 'email' in data
}
if (isUser(data)) {
// data 安全使用
}
陷阱 3:ref 不写泛型——ref() 不写泛型,推导为 Ref,后续使用没有类型提示。
// ❌ 不写泛型——推导为 any
const count = ref() // Ref<any>
const user = ref(null) // Ref<any>
// ✅ 显式泛型
const count = ref<number>(0) // Ref<number>
const user = ref<User | null>(null) // Ref<User | null>
陷阱 4:Props 不完整声明——只声明部分 Props,其余变成 any。
// ❌ 不完整声明
defineProps<{
title: string
// 忘了声明 items,模板中 items 是 any
}>()
// ✅ 完整声明
defineProps<{
title: string
items: string[]
loading?: boolean
}>()
陷阱 5:reactive 类型丢失——reactive() 的类型推导不如 ref 精确。
// ❌ reactive 类型不精确
const state = reactive({
list: [], // never[] 而不是 User[]
count: 0,
})
// ✅ 用接口约束
interface State {
list: User[]
count: number
}
const state: State = reactive({
list: [],
count: 0,
})
3.2 TypeScript + Vue3 最佳实践清单
| 规则 | 做法 | 原因 |
|---|---|---|
| 不用 any | 用 unknown + 类型守卫 | any 是类型黑洞 |
| 不滥用 as | 用类型守卫/泛型 | as 是掩耳盗铃 |
| ref 显式泛型 | ref<T>() |
否则推导为 any |
| Props 完整声明 | 所有 Props 都声明 | 否则变成 any |
| reactive 用接口 | const state: T = reactive() |
类型更精确 |
| Composable 返回类型 | 显式声明返回类型 | IDE 提示更好 |
| 导入类型用 type | import type { X } |
避免运行时引入 |
| 事件类型精确 | defineEmits 泛型 | 参数自动检查 |
| 泛型组件约束 | T extends { id } |
约束泛型范围 |
| tsconfig 严格 | strict: true |
开启所有检查 |
Vue3 实战系列完结
| 篇号 | 主题 | 核心内容 | 状态 |
|---|---|---|---|
| 01 | 组合式API | ref/reactive/computed/watch/Composables | ✅ |
| 02 | 组件通信 | props/emit/provide-inject/v-model | ✅ |
| 03 | Pinia状态管理 | Store/Action/持久化/模块化 | ✅ |
| 04 | Router路由 | 动态路由/守卫/懒加载 | ✅ |
| 05 | 性能优化 | 虚拟列表/异步组件/SSR | ✅ |
| 06 | TypeScript+Vue3(本文) | 类型安全/泛型组件/类型推导 | ✅ |
一句话总结
TypeScript + Vue3 三大维度:类型安全基础(defineProps泛型声明>运行时声明——类型即文档IDE自动补全复杂类型联合类型函数类型都支持/defineEmits泛型声明——事件类型安全参数自动检查传错类型编译报错/ref模板类型InstanceType——获取子组件实例类型调用expose方法有提示。Vue3+TS的核心=用类型约束Props/Emit/Ref让错误在编译时暴露而非运行时)、泛型组件与Composable(泛型组件generic=“T” Vue 3.3+——一个List组件适配User[]/Product[]/Order[]/keyof T约束属性名传错编译报错/T extends {id:number}约束泛型范围。泛型Composable function()——useFetch<User[]>自动推导data类型/useLocalStorage自动推导存储值类型。泛型的价值=写一次类型安全的代码适配所有数据类型DRY原则的终极实现)、常见陷阱与最佳实践(五大陷阱——any类型黑洞用unknown+类型守卫/as掩耳盗铃用类型守卫或泛型/ref不写泛型推导为any/Props不完整声明变成any/reactive类型丢失用接口约束。最佳实践清单——不用any/不滥用as/ref显式泛型/Props完整声明/reactive用接口/Composable返回类型/import type/事件类型精确/泛型组件约束/tsconfig strict:true。Vue3+TS的终极目标=让错误在编译时暴露让IDE成为最好的搭档)。
参考链接:
更多推荐
所有评论(0)