《Vue3 从入门到大神24篇》TypeScript 深度集成 —— 给 Vue3 加上强类型保护

📅 2026/7/7 2:34:26
《Vue3 从入门到大神24篇》TypeScript 深度集成 —— 给 Vue3 加上强类型保护
前言Vue3 从立项之初就是用TypeScript 重写的。这意味着TypeScript 不是 Vue3 的插件而是它的母语。但在实际项目中很多开发者虽然开了langts却只是const count: any ref(0) // ❌ 等于没用 TS这一篇我们把Vue3 TypeScript 的正确打开方式​ 系统梳理一遍覆盖✅ Props / Emits 类型推导✅ Composable 类型设计✅ Pinia 类型化✅ 泛型组件✅ 常见类型坑位一、为什么 Vue3 TS 是天作之合维度Vue2 TSVue3 TS源码FlowTypeScript类型推导有限完整Options API需要装饰器支持但非首选Composition API不友好✅ 原生适配工具链配置复杂Volar 开箱即用Vue3 的 Composition API 天然就是函数而函数是 TS 最喜欢的抽象方式。二、Props 与 Emits 的类型化1️⃣ Props 类型声明推荐 ✅script setup langts interface Props { title: string count?: number disabled?: boolean } const props definePropsProps() /script✅IDE 自动提示 编译期校验2️⃣ 带默认值的 Propsinterface Props { title: string count?: number } const props withDefaults(definePropsProps(), { count: 0 })3️⃣ Emits 类型声明interface Emits { (e: change, value: number): void (e: submit, payload: { id: number; name: string }): void } const emit defineEmitsEmits() // 调用时自动提示参数类型 emit(change, 100) emit(submit, { id: 1, name: Tom })4️⃣ 完整组件示例script setup langts interface Props { user: { id: number name: string email: string } editable?: boolean } interface Emits { (e: update, user: Props[user]): void (e: delete, id: number): void } const props withDefaults(definePropsProps(), { editable: false }) const emit defineEmitsEmits() const handleSave () { emit(update, { ...props.user, name: New Name }) } /script三、ref / reactive 的类型推断1️⃣ ref 的泛型参数import { ref } from vue // 自动推断为 Refnumber const count ref(0) // 指定泛型 interface User { id: number name: string } const user refUser | null(null)2️⃣ reactive 的类型约束interface State { users: User[] loading: boolean page: number } const state reactiveState({ users: [], loading: false, page: 1 })3️⃣ computed 的类型推断const doubleCount computed(() count.value * 2) // 自动推断为 ComputedRefnumber四、Composable 的类型设计1️⃣ 基础 Composable// composables/useCounter.ts import { ref, computed } from vue export function useCounter(initial 0) { const count ref(initial) const double computed(() count.value * 2) const inc () count.value const dec () count.value-- return { count, double, inc, dec } }2️⃣ 带泛型的 Composable// composables/useToggle.ts import { ref } from vue export function useToggle(defaultValue false) { const value ref(defaultValue) const toggle () { value.value !value.value } return { value, toggle } }3️⃣ 异步 Composable// composables/useFetch.ts import { ref } from vue export function useFetchT(url: string) { const data refT | null(null) const loading ref(false) const error refstring | null(null) const fetchData async () { loading.value true error.value null try { const res await fetch(url) data.value await res.json() } catch (e: any) { error.value e.message } finally { loading.value false } } return { data, loading, error, fetchData } }使用示例interface User { id: number name: string } const { data, loading, fetchData } useFetchUser[](/api/users)五、Pinia 的类型化1️⃣ Option Store 类型// stores/user.ts import { defineStore } from pinia interface User { id: number name: string roles: string[] } export const useUserStore defineStore(user, { state: () ({ user: null as User | null, token: }), getters: { isAdmin(): boolean { return this.user?.roles.includes(admin) ?? false } }, actions: { setUser(user: User) { this.user user } } })2️⃣ Setup Store 类型export const useUserStore defineStore(user, () { const user refUser | null(null) const token ref() const isAdmin computed(() { return user.value?.roles.includes(admin) ?? false }) const setUser (u: User) { user.value u } return { user, token, isAdmin, setUser } })3️⃣ Store 的类型提取import type { useUserStore } from /stores/user type UserStore ReturnTypetypeof useUserStore六、泛型组件1️⃣ 定义泛型组件script setup langts genericT defineProps{ items: T[] keyField: keyof T }() const emit defineEmits{ (e: select, item: T): void }() /script template div v-foritem in items :keyitem[keyField] clickemit(select, item) slot :itemitem/slot /div /template2️⃣ 使用泛型组件script setup langts interface Product { id: number name: string price: number } const products refProduct[]([...]) /script template GenericList :itemsproducts key-fieldid selecthandleSelect template #default{ item } {{ item.name }} - ¥{{ item.price }} /template /GenericList /template七、Provide / Inject 的类型化// keys.ts import type { InjectionKey } from vue import type { User } from /types export const USER_KEY Symbol(user) as InjectionKeyUser // 提供方 provide(USER_KEY, user) // 注入方 const user inject(USER_KEY)八、常见类型坑位与解决方案❌ 坑 1defineProps 解构丢失类型const { count } defineProps{ count: number }() // ❌✅正确做法const props defineProps{ count: number }() const { count } toRefs(props)❌ 坑 2Event Handler 类型const handleClick (e: any) {} // ❌✅正确做法const handleClick (e: MouseEvent) {}❌ 坑 3Template Ref 类型const inputRef ref(null) // 类型是 Refnull✅正确做法const inputRef refHTMLInputElement | null(null) // 使用时 inputRef.value?.focus()❌ 坑 4第三方库类型缺失import someLib from some-lib // 没有类型声明✅解决方案// shims-some-lib.d.ts declare module some-lib { const value: any export default value }九、TSConfig 推荐配置{ compilerOptions: { target: ESNext, module: ESNext, moduleResolution: bundler, strict: true, jsx: preserve, sourceMap: true, resolveJsonModule: true, isolatedModules: true, esModuleInterop: true, lib: [ESNext, DOM], types: [vite/client] } }strict: true是必须的否则类型检查形同虚设。十、面试高频问答Q1Vue3 为什么对 TypeScript 支持更好因为源码用 TS 重写Composition API 是函数式设计天然适合类型推导。Q2defineProps 的两种写法有什么区别运行时写法支持校验编译时写法interface提供更好的类型推导。Q3如何在 Vue3 中正确使用泛型使用script setup langts genericT语法。十一、总结架构级Vue3 TS 是工程化标配Props / Emits 用 interface 声明Composable 是 TS 的最佳实践场景Pinia 天生类型友好泛型组件提升组件复用性 下期预告第 25 篇手撕 Vue3 响应式原理 —— 手写 Mini-Vue一