Vue3 + TypeScript 实战:给组件、Props、Pinia 加上类型约束,代码稳如老狗
一、先聊五毛钱为啥要加 TypeScript之前咱们写的 Vue 代码都是 JavaScript。说实话小项目用 JS 完全没问题。但项目一大、人一多问题就来了你写了一个函数接收一个对象参数时间一长根本记不住这个对象里有哪些字段、什么类型。同事改了你组件的一个 prop从数字改成了字符串你的代码直接崩了但控制台没有任何提示。从后端拿到一串数据深层嵌套你得console.log半天才知道它长啥样。TypeScript 就是给 JavaScript 加了一套“类型标注”系统。你在写代码时提前说清楚这个变量是字符串、那个参数是对象、这个函数返回的是数组……之后不管是你自己、同事还是编辑器都能知道这些信息。写错了立马有提示不用等到运行时才报错。一句话总结TypeScript 就像给代码装上了一个实时纠错的“语法老师”。二、第一步在 Vite Vue3 项目里启用 TypeScript如果你创建项目时勾选了 TypeScript那环境已经有了。如果没勾也不用慌在项目根目录执行bashnpm install -D typescript然后新建一个tsconfig.json贴入以下配置最简版json{ compilerOptions: { target: ES2020, module: ESNext, moduleResolution: node, strict: true, jsx: preserve, sourceMap: true, resolveJsonModule: true, esModuleInterop: true, lib: [ES2020, DOM, DOM.Iterable], skipLibCheck: true, baseUrl: ., paths: { /*: [src/*] } }, include: [src/**/*.ts, src/**/*.d.ts, src/**/*.vue], exclude: [node_modules] }最关键的是strict: true它开启了最严格的类型检查。刚开始可能觉得烦但只有这样才能真正发挥 TS 的威力。如果你的组件还是.js后缀改成.vue里的script setup langts就行了。三、第二步在script setup中使用 TypeScript3.1 ref 的类型标注ref可以根据初始值自动推断类型但有时候我们需要显式指定。vuescript setup langts import { ref } from vue // 1. 自动推断count 被推断为 refnumber const count ref(0) // 2. 显式泛型标注明确告诉 ref 这是字符串 const name refstring(小明) // 3. 复杂类型比如一个用户对象 interface User { id: number name: string email?: string // ? 表示可选 } const user refUser({ id: 1, name: 小明 }) // 4. 可以为 null 的类型联合类型 const timer refnumber | null(null) /script解释refnumber是泛型语法表示这个 ref 里面存的是number类型。interface User定义了一个用户对象的“形状”之后user就有了自动提示。timer初始值是null但以后会存数字定时器 ID所以用number | null联合类型表示“可能是数字也可能是 null”。3.2 reactive 的类型标注vuescript setup langts import { reactive } from vue // 定义一个表单对象的接口 interface LoginForm { username: string password: string remember: boolean } // 使用接口标注 reactive 的类型 const form reactiveLoginForm({ username: , password: , remember: false }) // 现在 form 对象里的属性有自动补全和类型检查 form.username admin // 正确 // form.username 123 // 错误不能将数字赋值给字符串 /script3.3 computed 的类型computed也会自动推断返回类型也可以显式标注。vuescript setup langts import { ref, computed } from vue const price ref(100) const count ref(2) // 自动推断为 ComputedRefnumber const total computed(() price.value * count.value) // 显式标注需要时 const summary computedstring(() { return 总价${total.value} 元 }) /script四、第三步Props 和 Emits 的类型标注重点在script setup中defineProps和defineEmits都可以直接用 TypeScript 类型来定义比传统的对象写法更简洁。4.1 Props 的 TS 写法vue!-- 子组件 UserCard.vue -- template div p姓名{{ name }}/p p年龄{{ age }}/p /div /template script setup langts // 使用 defineProps 的泛型参数来定义类型 const props defineProps{ name: string // 必填字符串 age?: number // 可选数字? 表示可以不传 hobbies?: string[] // 可选字符串数组 }() // 现在 props.name 是 stringprops.age 是 number | undefined console.log(props.name.toUpperCase()) // 没问题 /script带默认值的写法使用 withDefaultsvuescript setup langts interface Props { name: string age?: number gender: male | female // 联合类型只能是这两个值 } const props withDefaults(definePropsProps(), { age: 18, gender: male }) /script解释defineProps{...}()直接在尖括号里定义类型不需要对象语法。withDefaults用于设置默认值第一个参数是definePropsProps()第二个是默认值对象。注意name: string没有?是必填有?的是可选的。4.2 Emits 的 TS 写法vuescript setup langts // 定义事件及其参数类型 const emit defineEmits{ // 事件名 update: 参数是 string 类型 update: [value: string] // 事件名 delete: 参数是 number 类型 delete: [id: number] // 事件名 submit: 没有参数 submit: [] }() // 调用时参数会自动检查类型 function handleUpdate() { emit(update, 新的值) // 正确 // emit(update, 123) // 错误需要 string 类型 } function handleDelete() { emit(delete, 1) // 正确 } /script解释defineEmits{...}()里键是事件名值是一个元组[参数类型]。如果事件有多个参数可以(e: change, id: number, name: string) void这种写法但最常用的是上面那种。五、第四步组合式函数的 TS 类型之前我们封装了useAsync现在给它加上完整的类型。typescript// hooks/useAsync.ts import { ref, type Ref } from vue // 定义一个泛型接口表示 useAsync 的返回值 interface UseAsyncResultT { data: RefT | null // 数据类型是 T 或 null loading: Refboolean error: Refstring | null execute: (...args: any[]) PromiseT } // asyncFn 是一个返回 PromiseT 的函数 export function useAsyncT( asyncFn: (...args: any[]) PromiseT, options?: { immediate?: boolean } ): UseAsyncResultT { const data refT | null(null) as RefT | null const loading ref(false) const error refstring | null(null) async function execute(...args: any[]) { loading.value true error.value null try { const result await asyncFn(...args) data.value result return result } catch (err: any) { error.value err.message || 请求失败 throw err } finally { loading.value false } } if (options?.immediate) { execute() } return { data, loading, error, execute } }在组件中使用vuescript setup langts import { useAsync } from /hooks/useAsync import { getUserInfo } from /api/user // 这里 T 被推断为返回的 data 类型 const { data, loading, error } useAsync(getUserInfo, { immediate: true }) // data 的类型是 RefUserInfo | null自动有提示 /script六、第五步Pinia 的 TS 支持Pinia 对 TS 的支持非常好写法和之前差不多只是多了类型定义。typescript// stores/user.ts import { defineStore } from pinia import { ref, computed } from vue // 定义用户对象的接口 interface UserInfo { id: number name: string email: string } export const useUserStore defineStore(user, () { // 使用泛型指定 ref 的类型 const user refUserInfo | null(null) const token ref() // 计算属性自动推断类型 const isLogin computed(() !!token.value) // 登录方法参数标注类型 function login(userData: UserInfo, t: string) { user.value userData token.value t } function logout() { user.value null token.value } return { user, token, isLogin, login, logout } })组件中使用自动提示vuescript setup langts import { useUserStore } from /stores/user const userStore useUserStore() // userStore.user 的类型是 RefUserInfo | null console.log(userStore.user?.name) // 有 ?. 因为可能为 null /script七、实战案例用 TypeScript 重写登录表单把之前写的登录页用 TS 改造完整展示 props、emits、表单类型、事件类型。vue!-- LoginForm.vue -- template form submit.preventhandleSubmit div label用户名/label input v-modelform.username / /div div label密码/label input v-modelform.password typepassword / /div button typesubmit{{ submitText }}/button /form /template script setup langts import { reactive } from vue // 定义表单数据类型 interface LoginForm { username: string password: string } // Props 类型 interface Props { submitText?: string } const props withDefaults(definePropsProps(), { submitText: 登录 }) // Emits 类型 const emit defineEmits{ submit: [form: LoginForm] }() // 表单数据 const form reactiveLoginForm({ username: , password: }) function handleSubmit() { // 触发 submit 事件传递表单数据 emit(submit, { ...form }) } /script父组件使用vuetemplate LoginForm submit-text立即登录 submithandleLogin / /template script setup langts import LoginForm from ./LoginForm.vue function handleLogin(form: { username: string; password: string }) { console.log(提交的表单, form) } /script八、总结今天我们给 Vue3 加上了 TypeScript核心内容包括场景TS 写法ref 类型const count refnumber(0)reactive 类型reactiveMyType({...})PropsdefineProps{ name: string; age?: number }()带默认值 PropswithDefaults(definePropsProps(), {...})EmitsdefineEmits{ submit: [form: LoginForm] }()组合式函数function useAsyncT(...)Piniaconst user refUserInfo | null(null)一句话记住TypeScript 不是给代码增加负担而是提前把错误扼杀在摇篮里。刚开始可能会被类型报错整崩溃但坚持一周你会发现代码居然一次就跑通的概率大大增加。有问题评论区说我挨个回。下篇咱们聊Vue3 架构设计把目录结构和代码分层搞明白让项目更好维护