14-TypeScript 与 Vue3

📅 2026/7/1 12:55:43
14-TypeScript 与 Vue3
TypeScript 与 Vue3Vue3 从底层重构了类型系统配合script setup langts让 TypeScript 开发体验达到全新高度。一、前言TypeScript 为 JavaScript 提供了静态类型检查能够在编译阶段发现潜在错误提升代码的可维护性和开发效率。Vue3 使用 TypeScript 完全重写了核心源码提供了开箱即用的类型支持。相比 Vue2 需要依赖vue-class-component或复杂的类型声明Vue3 的 Composition API 与 TypeScript 结合更加自然。本文将系统讲解 Vue3 TypeScript 的开发实践。二、script setup langts基础2.1 启用 TypeScript 支持在 Vue3 单文件组件中只需添加langts即可使用 TypeScriptscript setup langts import { ref } from vue // TypeScript 会自动推断类型 const count ref(0) // Refnumber const message ref(hello) // Refstring const isShow ref(true) // Refboolean /script2.2 类型推断与显式声明Vue3 的响应式 API 具有良好的类型推断能力但在复杂场景下建议显式声明类型script setup langts import { ref, reactive } from vue // 自动推断 const autoCount ref(0) // Refnumber // 显式声明推荐用于复杂类型 const count refnumber(0) const name refstring(Vue3) const items refstring[]([a, b, c]) // 接口定义 interface User { id: number name: string email?: string // 可选属性 } const user refUser({ id: 1, name: 张三 }) // reactive 的类型推断 const state reactive({ count: 0, user: { name: 李四 } as User }) /script三、Props 类型声明3.1 使用类型字面量script setup langts // 简单类型声明 const props defineProps{ title: string count?: number // 可选 items: string[] user: { name: string; age: number } }() /script3.2 使用接口定义script setup langts // 接口定义推荐可复用 interface Props { title: string count?: number disabled?: boolean } const props definePropsProps() /script3.3 带默认值的 Props使用withDefaults编译器宏设置默认值script setup langts interface Props { title: string count?: number disabled?: boolean tags?: string[] } const props withDefaults(definePropsProps(), { count: 0, disabled: false, tags: () [default] // 对象/数组默认值需用工厂函数 }) /script3.4 复杂的 Props 类型script setup langts // 定义枚举类型 type ButtonType primary | success | warning | danger type ButtonSize small | medium | large interface Props { type?: ButtonType size?: ButtonSize loading?: boolean // 函数类型 Props onClick?: (event: MouseEvent) void // 对象数组 options: Array{ label: string value: string | number disabled?: boolean } } const props withDefaults(definePropsProps(), { type: primary, size: medium, loading: false }) /script四、Emits 类型声明4.1 基本用法script setup langts // 声明 emits 及其参数类型 const emit defineEmits{ // 无参数事件 click: [] // 单参数事件 update: [value: string] // 多参数事件 submit: [data: FormData, callback: (result: boolean) void] // 可选参数事件 change: [value?: number] }() const handleClick () { emit(click) } const handleUpdate (value: string) { emit(update, value) } /script4.2 与 v-model 配合!-- InputField.vue -- template input :valuemodelValue input$emit(update:modelValue, ($event.target as HTMLInputElement).value) / /template script setup langts const props defineProps{ modelValue: string }() const emit defineEmits{ update:modelValue: [value: string] }() /script4.3 多个 v-modelscript setup langts interface Props { title: string content: string } const props definePropsProps() const emit defineEmits{ update:title: [value: string] update:content: [value: string] }() /script template div input :valuetitle inputemit(update:title, ($event.target as HTMLInputElement).value) / textarea :valuecontent inputemit(update:content, ($event.target as HTMLTextAreaElement).value) / /div /template五、响应式 API 的类型5.1 ref 的类型script setup langts import { ref, Ref } from vue // 基本类型 const count: Refnumber ref(0) // 对象类型 interface User { name: string age: number } const user refUser({ name: 张三, age: 25 }) // user.value 的类型为 User // 可能为 null 的引用常用于 DOM 引用 const inputRef refHTMLInputElement | null(null) // 数组类型 const list refnumber[]([1, 2, 3]) // 联合类型 const status refidle | loading | success | error(idle) /script5.2 computed 的类型script setup langts import { ref, computed } from vue const firstName ref(张) const lastName ref(三) // 自动推断返回类型为 string const fullName computed(() ${firstName.value} ${lastName.value}) // 显式声明类型 const age refstring | number(25) const ageDisplay computedstring(() ${age.value} 岁) // 可写 computed const count ref(0) const doubleCount computed({ get: (): number count.value * 2, set: (val: number) { count.value val / 2 } }) /script5.3 reactive 的类型script setup langts import { reactive } from vue // 接口定义 interface FormState { username: string password: string remember: boolean errors: Recordstring, string[] } const form reactiveFormState({ username: , password: , remember: false, errors: {} }) // 使用类型断言处理可选属性 interface Config { apiUrl: string timeout?: number } const config reactiveConfig({ apiUrl: /api // timeout 是可选的可以不提供 }) /script六、组件类型6.1 组件实例类型script setup langts import { ref } from vue import ChildComponent from ./ChildComponent.vue // 获取子组件实例类型 const childRef refInstanceTypetypeof ChildComponent | null(null) const callChildMethod () { // TypeScript 知道 childRef.value 上有哪些方法 childRef.value?.someMethod() } /script template ChildComponent refchildRef / /template6.2 事件类型script setup langts // 模板引用事件处理 const handleInput (event: Event) { const target event.target as HTMLInputElement console.log(target.value) } // 键盘事件 const handleKeydown (event: KeyboardEvent) { if (event.key Enter) { console.log(按下了回车键) } } // 鼠标事件 const handleMouseMove (event: MouseEvent) { console.log(event.clientX, event.clientY) } /script template input inputhandleInput keydownhandleKeydown / div mousemovehandleMouseMove移动鼠标/div /template6.3 全局组件类型声明在components.d.ts中声明全局组件// components.d.tsimportMyGlobalComponentfrom./src/components/MyGlobalComponent.vuedeclaremodulevue{exportinterfaceGlobalComponents{MyGlobalComponent:typeofMyGlobalComponent}}export{}七、TSX / JSX 在 Vue3 中的使用7.1 基本 TSX 组件// HelloWorld.tsx import { ref, defineComponent } from vue interface Props { name: string count?: number } export default defineComponent({ props: [name, count] as const, setup(props: Props) { const internalCount ref(props.count || 0) const increment () { internalCount.value } return () ( div classhello h1Hello, {props.name}!/h1 pCount: {internalCount.value}/p button onClick{increment}Increment/button /div ) } })7.2 使用script setup风格的 TSX// Counter.tsx import { ref } from vue interface Props { initial?: number step?: number } const props withDefaults(definePropsProps(), { initial: 0, step: 1 }) const emit defineEmits{ change: [value: number] }() const count ref(props.initial) const increment () { count.value props.step emit(change, count.value) } export default () ( div classcounter span{count.value}/span button onClick{increment}{props.step}/button /div )7.3 TSX 类型配置在tsconfig.json中配置 JSX{compilerOptions:{jsx:preserve,jsxImportSource:vue}}八、类型安全的 Pinia Store8.1 定义类型安全的 Store// stores/user.tsimport{defineStore}frompiniaimport{ref,computed}fromvue// 定义用户接口exportinterfaceUser{id:numbername:stringemail:stringavatar?:string}// 定义 Store 状态接口exportinterfaceUserState{user:User|nulltoken:string|nullisLoggedIn:boolean}exportconstuseUserStoredefineStore(user,(){// StateconstuserrefUser|null(null)consttokenrefstring|null(localStorage.getItem(token))// GettersconstisLoggedIncomputed(()!!token.value!!user.value)constuserNamecomputed(()user.value?.name||访客)// ActionsconstsetUser(userData:User){user.valueuserData}constsetToken(newToken:string){token.valuenewToken localStorage.setItem(token,newToken)}constloginasync(credentials:{email:string;password:string}){// 模拟 API 调用constresponseawaitfetch(/api/login,{method:POST,body:JSON.stringify(credentials)})constdataawaitresponse.json()as{user:User;token:string}setUser(data.user)setToken(data.token)}constlogout(){user.valuenulltoken.valuenulllocalStorage.removeItem(token)}return{user,token,isLoggedIn,userName,setUser,setToken,login,logout}})8.2 在组件中使用script setup langts import { useUserStore } from /stores/user const userStore useUserStore() // TypeScript 提供完整的类型提示 console.log(userStore.userName) // string console.log(userStore.isLoggedIn) // boolean const handleLogin async () { await userStore.login({ email: userexample.com, password: password123 }) } /script九、常见类型问题与解决方案9.1 ref 的解包问题script setup langts import { ref, unref } from vue const count ref(0) // 在模板中 ref 会自动解包 // 在 JS 中需要 .value console.log(count.value) // 使用 unref 处理可能是 ref 的值 function useMaybeRef(maybeRef: number | Refnumber) { const value unref(maybeRef) console.log(value) // number } /script9.2 响应式对象解构丢失响应性script setup langts import { reactive, toRefs } from vue interface State { count: number name: string } const state reactiveState({ count: 0, name: Vue }) // 错误解构会丢失响应性 // const { count, name } state // 正确使用 toRefs const { count, name } toRefs(state) // 现在 count 和 name 都是 Ref console.log(count.value) console.log(name.value) /script9.3 模板引用类型script setup langts import { ref, onMounted } from vue // 元素引用 const inputRef refHTMLInputElement | null(null) // 组件引用 const childRef ref{ focus: () void } | null(null) onMounted(() { // TypeScript 会提示可能为 null inputRef.value?.focus() childRef.value?.focus() }) /script template input refinputRef / ChildComponent refchildRef / /template9.4 第三方库类型扩展// types/shims.d.tsimport{ComponentCustomProperties}fromvueimport{Store}frompiniadeclaremodulevue/runtime-core{interfaceComponentCustomProperties{$store:Store}}// 为全局属性添加类型declaremodulevue{interfaceComponentCustomProperties{$http:typeoffetch}}9.5 泛型组件!-- GenericList.vue -- script setup langts genericT extends { id: number } interface Props { items: T[] keyProp?: keyof T } const props definePropsProps() const emit defineEmits{ select: [item: T] }() /script template ul li v-foritem in items :keyitem.id clickemit(select, item) slot :itemitem / /li /ul /template使用泛型组件script setup langts import GenericList from ./GenericList.vue interface User { id: number name: string age: number } const users: User[] [ { id: 1, name: 张三, age: 25 }, { id: 2, name: 李四, age: 30 } ] const handleSelect (user: User) { console.log(user.name) } /script template GenericList :itemsusers selecthandleSelect template #default{ item } {{ item.name }} - {{ item.age }}岁 /template /GenericList /template十、类型系统架构图Vue3 TypeScript组件类型响应式类型Props/Emits 类型Store 类型TSX/JSX 类型InstanceType模板引用类型全局组件声明RefTReactiveTComputedRefTdefinePropsPropsdefineEmitsEventswithDefaultsPinia StoreState 接口Action 类型defineComponentJSX.Element泛型组件十一、常见问题Q1为什么ref(null)推断为Refany当没有提供初始值或初始值为null时TypeScript 无法推断具体类型。需要显式声明constelrefHTMLDivElement|null(null)constuserrefUser|null(null)Q2如何解决defineProps的复杂类型报错对于复杂类型如从其他文件导入的接口确保类型是字面量类型或接口// 推荐使用接口interfaceProps{...}constpropsdefinePropsProps()// 避免使用复杂的类型工具// const props definePropsReturnTypetypeof useProps() // 可能报错Q3TypeScript 严格模式下ref的undefined问题// 在 strictNullChecks 模式下constmaybeUserrefUser()// RefUser | undefined// 访问时需要判断if(maybeUser.value){console.log(maybeUser.value.name)}// 或使用非空断言谨慎使用console.log(maybeUser.value!.name)Q4如何为 Vue Router 添加类型支持// typed-router.d.tsimportvue-routerdeclaremodulevue-router{interfaceRouteMeta{requiresAuth?:booleantitle?:stringroles?:string[]}}十二、总结Vue3 的 TypeScript 支持是框架的核心优势之一特性Vue2Vue3源码语言Flow / JavaScriptTypeScriptProps 类型运行时校验编译时类型 运行时校验Emits 类型无完整类型支持响应式类型有限完整泛型支持TSX 支持需额外配置原生支持类型推断较弱优秀核心要点使用langts启用 TypeScript 支持用接口定义 Props 类型提高可复用性defineEmits使用元组语法声明事件参数复杂类型使用withDefaults设置默认值Pinia 配合 TypeScript 提供完整的 Store 类型安全善用泛型组件处理列表等通用场景十三、练习题将现有的一个 Vue3 组件改写为完整的 TypeScript 版本包括 Props、Emits、模板引用的类型声明。使用 TypeScript 定义一个表单验证 Hook要求支持泛型表单数据类型类型安全的验证规则自动推断错误信息类型创建一个类型安全的 Event Bus或使用 mitt确保事件名称和参数类型在发布和订阅时一致。