ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3 + TypeScript 实战指南:从组件定义到响应式数据类型安全

Vue3 + TypeScript 实战指南:从组件定义到响应式数据类型安全 有段时间没写前端相关的内容了最近在一个中型后台管理项目里从零接入了 Vue3 TypeScript虽然前期踩了不少坑但整体跑下来收益是真的明显。今天我就把自己在“Vue3 怎么搭 TypeScript、怎么定义组件和 props 类型、怎么给响应式数据加约束”这条路上的实战经验整理出来希望能帮到正在从 JS 往 TS 过渡的同学。先说下我自己的情况之前 Vue2 用了一年多一直都是 JavaScript 一把梭组件一多、props 一乱、接口字段一改靠人脑记类型真的是灾难。后来项目要扩展新页面全部改用 Vue3 TypeScript 写刚开始确实别扭但用顺之后我自己是再也回不去纯 JS 写大项目了。这篇文章我会按实际项目落地的顺序来讲不整虚的从环境搭建、组件写法、props 定义、响应式数据约束、通用类型的抽离到常见报错排查一条龙走完。你自己要在项目里实践的话可以直接照着敲基本不踩坑。1. 为什么要用 TypeScript 结合 Vue3 写业务代码1.1 从 Vue2 的“自由”到 Vue3 的“约束”Vue2 时代我们用 JavaScript 写组件data 里定义变量、props 接收参数、methods 里处理逻辑一切都靠“约定”。小项目没事但代码量一上来到处都是“这个字段到底是不是字符串”、“那个对象里有没有这个属性”的疑问。我记得有次改接口字段全局搜了半天最后还是漏了一个组件线上直接渲染 crash排查了两个小时。整个过程就是浪费时间靠人肉记忆维护跨文件的类型契约基本等于裸奔。Vue3 发布后官方整条链路都对 TypeScript 做了深度支持而且是从设计层面就跟 TypeScript 对齐的。比如 setup 语法、ref 函数、reactive 函数、defineProps 这些 API全部都是类型友好的。这意味着你用 TypeScript 写 Vue3不是生硬凑合而是顺着框架的设计思路来。组件之间传递的东西到底长什么样不再是“你猜”而是“编译器帮你查”。TypeScript 的本质其实是给 JavaScript 加了一套静态类型系统在编译阶段就能发现大量低级错误。放到 Vue3 里最大的收益体现在三个地方props 传参不对即刻报错、接口返回的数据结构有据可查、代码重构时有 IDE 全局提示。这三样分别解决的是“传错了”、“猜错了”、“改漏了”的问题。1.2 类型系统到底解决了什么痛点我用一个特别直白的类比来说明。JavaScript 写代码就像在纸上画电路图能不能通全靠你经验丰富不丰富TypeScript 相当于用了一个原理图检查工具哪根线接错了、哪个电阻阻值不合适画的时候就能校验出来。类型定义是给代码里的每个变量、参数、返回值都贴上一张“规格说明书”让人和编译器都能快速判断它是干嘛的。在实际项目里类型系统带来的直接好处是组件传参时父组件写错 prop 名或者类型不匹配编辑器会直接标红。接口的请求函数和数据处理函数之间有了约定后端改字段后前端哪里要改一目了然。重构组件、修改字段名时IDE 可以安全地全局重命名不会漏改。这些东西在写单页小 Demo 时感受不明显一旦进入中型项目多人协作、几十上百个组件互相调用类型系统带来的安全感是无法替代的。2. 搭建 Vue3 TypeScript 开发环境2.1 使用 Vite 快速初始化项目Vue3 官方现在推荐的构建工具就是 Vite配 TypeScript 非常丝滑。我用的是 npm 的方式创建项目命令如下npm create vitelatest vue3-ts-app -- --template vue-ts这里我解释一下这个命令干了什么。npm create vitelatest会拉取最新的 Vite 脚手架vue-ts是它内置的一个模板等于直接帮你把 Vue3、TypeScript、以及两者结合的基础配置都准备好。项目创建完成后进入目录安装依赖cd vue3-ts-app npm install装完之后你要会看项目根目录下的 tsconfig.json这是 TypeScript 编译器的配置文件。默认的vue-ts模板会生成一个相对合理的配置但我通常会做一点调整让它更贴近自己的开发习惯。核心是这三个字段实际项目里一定要明白它们的意思target编译输出的 JavaScript 版本我一般写ES2020。写太低有些新语法会编译出很冗余的代码写太高可能不支持老浏览器。moduleResolution模块解析策略Vite 项目建议用bundler这个能更好地配合现代打包工具。strict严格模式开关必须设成true。很多新手为了少报错会关掉 strict我的建议是千万别关后面你会感谢这个决定的。如果你看到编辑器提示选项“moduleResolutionnode10”已弃用不用慌直接把tsconfig.json里 moduleResolution 改成bundler就行。这是 TypeScript 新版对配置项更严格导致的。刚开始不熟悉的话可以先把这份配置作为最小可用版本{ compilerOptions: { target: ES2020, useDefineForClassFields: true, module: ESNext, moduleResolution: bundler, strict: true, jsx: preserve, resolveJsonModule: true, isolatedModules: true, esModuleInterop: true, lib: [ES2020, DOM, DOM.Iterable], skipLibCheck: true, noEmit: true }, include: [src/**/*.ts, src/**/*.d.ts, src/**/*.tsx, src/**/*.vue], references: [{ path: ./tsconfig.node.json }] }细心的同学可能注意到 include 里有.vue后缀这是因为 Vite 模板里还有一个env.d.ts文件它会告诉 TypeScript.vue文件是啥类型。任何以.vue结尾的 SFC单文件组件都需要通过这个声明文件来让 TS 识别不然 import 组件就会报“找不到模块”。2.2 认识 tsconfig 里的关键配置项如果说上一节只是“怎么搭”那这一节我展开讲讲“为什么要这样配”。先说strict: true。这个字段实质上是把 TypeScript 一系列严格检查的开关全打开了包括strictNullChecks空值检查、strictFunctionTypes函数类型检查等。最直观的感受是你定义一个变量可能为null如果不做收窄直接调用它的方法编辑器会直接报错。从短期看它增加了你写代码时要操的心但长期看它逼着你把代码里的每一个可能为空、每一个边界情况都考虑清楚。团队协作时别人接手你的代码也能明确知道哪个字段可能为空会主动做防御。再说moduleResolution: bundler。这个配置是 TypeScript 5.0 开始支持的专门给 Vite、Webpack 这类现代打包工具用的。它允许你在导入路径里省略.ts或.vue后缀同时按照打包工具的实际解析规则来找模块。如果配置成旧的node或者node10在解析.vue这种非标准模块时可能会出现类型找不到的情况而且新版 TypeScript 已经明确废弃了 node10出现了废弃提示。还有一个容易被忽略的是isolatedModules: true。它的意思是每个文件都作为独立模块编译这是 Vite 用 esbuild 做转译时的硬性要求。开启后有一个小坑需要注意你不能在同一个文件里用export { 某个类型 } from ./xxx这种写法再同时做类型和值的混合导出因为 esbuild 无法确定你导出的是类型还是值。实际上我们在 Vue3 项目里很少会遇到非要这么写的情况但知道了原理报错的时候就能快速定位。3. 组件模板与 TS 的结合SFC 写法与核心 API 的类型定义3.1 用script setup langts开启 TS 模式Vue3 的组合式 API 中最主流的写法就是script setup它比普通的“导出一个默认组件”的写法更简洁、更符合直觉。要启用 TypeScript只需要在 script 标签上加一个langtsscript setup langts import { ref } from vue const count refnumber(0) const increment () { count.value } /script template button clickincrement点击了 {{ count }} 次/button /template注意这里我写了refnumber(0)这就是给响应式数据加类型约束的最基础写法。ref在 TypeScript 中是一个泛型函数尖括号里的类型就是它内部值的类型。refnumber(0)表示 count 变量的.value只能是 number 类型如果后续代码里有人给它赋值字符串编辑器会直接报错。可能有人会问那我写ref(0)不写泛型可以吗可以TypeScript 会根据初始值自动推导出 number 类型。但如果你要定义的是一个复杂对象的 ref靠自动推导往往不够精确最好还是显式指定泛型。比如interface UserInfo { name: string age: number email?: string } const user refUserInfo({ name: 张三, age: 28 })这比不写类型强太多了。user.value什么都能塞的“野传统”直接结束后面接手的同事看代码一眼就知道这个 user 只有 name、age、email 三个字段。3.2 ref、reactive、computed 的泛型使用要点除了 refVue3 的reactive和computed也支持泛型。我分别说下实际项目里的用法和容易出错的地方。reactive适合定义对象类型的响应式数据它是用 Proxy 实现的直接访问属性就是响应的不需要像 ref 那样.value。它的泛型写法是import { reactive } from vue interface FormState { username: string password: string remember: boolean } const formState reactiveFormState({ username: , password: , remember: false })这里有个细节reactive的泛型类型不能太宽。比如你写reactiveRecordstring, any({})这样确实不会报错但等于完全放弃了类型检查。我的习惯是每个表单、每个模块的数据都要定义专门的 interface哪怕字段多点也没关系。computed的类型通常是自动推导的不需要手动写泛型。但有一种情况需要留意如果你的 computed 里可能有多个分支返回不同类型类型推导会变成一个联合类型这时候最好显式标注返回类型。举例const statusText computedstring(() { if (status.value success) { return 成功 } return 失败 })这个computedstring就是显式指定了计算属性的返回类型。虽然这个例子中不写也能推导出来但当你把计算逻辑抽成一个单独函数、或者后续要修改返回值结构时提前标注类型能让意图更明确。3.3 defineProps 的类型定义从运行时校验到编译期校验Vue2 和 Vue3 早期版本我们写 props 都是用一个对象来描述每个 prop 的类型、默认值、是否必填等这些信息在运行时会被 Vue 用来做校验。举个常见的例子props: { title: { type: String, required: true }, count: { type: Number, default: 0 } }这种写法“能用”但在 TypeScript 下属于绕远路。因为你在运行时定义类型之后TypeScript 编译器并不知道 props 具体包含什么字段、各自是什么类型。父组件传参时传错了TS 不会有任何提示。Vue3 的script setup提供了带泛型的defineProps写法可以直接把类型的定义和编译期校验合并成一步script setup langts interface Props { title: string count?: number visible: boolean } const props definePropsProps() /script这个写法有两个明显优势第一父组件如果传了不存在的 prop 名或者类型对不上比如该传 number 传了 stringVite 开发服务器的编辑器提示直接报红第二在组件内部使用props.xxx时IDE 会给出完整的类型提示和字段名补全手滑打错单词根本不可能发生。如果你需要给 prop 设置默认值类型定义和默认值通常要结合withDefaults来写script setup langts interface Props { title: string count?: number list?: string[] } const props withDefaults(definePropsProps(), { count: 0, list: () [] }) /script这里有一个非常重要的提醒withDefaults的第二个参数如果是引用类型对象、数组必须用函数返回新值比如list: () []不能直接写list: []。这个原因在于引用类型默认值如果直接给一个共享引用在多组件实例中可能会互相影响这是 Vue 底层设计的约束。我第一次写就踩了这个坑想着偷懒直接赋值结果 eslint 和 TS 双双报错。3.4 defineEmits 别随便 any事件类型也是契约类型约束的覆盖范围经常被忽略的就是自定义事件。刚开始我有段时间没格式化就进入事件回调的参数了结果一个问题排查了很久。后来用上了defineEmits的类型写法事件名、参数类型一目了然彻底告别这类“暗坑”。script setup langts interface Emits { (e: change, value: string): void (e: submit, formData: Recordstring, unknown): void } const emit defineEmitsEmits() const handleChange (val: string) { emit(change, val) } /script这样写的好处是父组件在监听change事件时能明确知道回调参数的类型不会出现回调写成(value: number) ...这种低级错误。事件也成了公开接口的一部分组件之间怎么协作所有改代码的人心里都有数。4. 响应式数据的类型定义与通用类型抽离4.1 接口数据的类型防御后端返回的坑前端项目里大量响应式数据来自接口而接口的数据结构说难听点后端队友哪天心情好加一个字段、哪天心情差改一个字段谁也说不准。TypeScript 没法在运行时帮前端劫持接口数据但它能在编译期做一层约束让我们在写数据处理时至少知道预期结构。我的习惯是每个接口模块单独创建一个api/types.ts文件把请求参数和响应数据的 interface 都放一起。比如一个典型的用户列表接口// api/user.ts export interface UserListItem { id: number name: string age: number avatar: string email?: string } export interface UserListParams { page: number pageSize: number keyword?: string } export interface UserListResponse { list: UserListItem[] total: number }然后再封装请求函数// api/user.ts import { request } from /utils/request export const fetchUserList (params: UserListParams) { return request.getUserListResponse(/api/user/list, { params }) }request.getUserListResponse这行代码表示请求返回的 data 会被强制当成 UserListResponse 类型来用。虽然它没法保证运行时数据结构真和这个 interface 一模一样但至少你在页面里取res.list、遍历item.name的时候全程都有类型提示。后端如果改了字段名前端只要对照 interface 调整即可比在十几个组件里到处搜字段名靠谱得多。前阵子接触了一个老项目接口返回的数据完全是“字典流”字段名全靠后端文档代码在运行时动不动就报 undefined 错误这就是缺乏类型契约的典型后果。4.2 响应式数据的 interface 定义技巧在实际业务里同样的数据结构可能在多个地方出现。比如用户信息列表页有精简版详情页有完整版编辑弹窗里可能又有一个表单版本。如果在每个组件里都重新定义一遍 interface代码会非常冗余。这时候就是抽公共类型的时机。一般我习惯在src/types目录下建文件按业务模块切割// types/user.ts export interface UserInfo { id: number username: string nickname: string avatar: string role: admin | editor | viewer status: active | disabled createdAt: string }然后在组件里直接 importscript setup langts import { ref } from vue import type { UserInfo } from /types/user const currentUser refUserInfo | null(null) const loadUser async (id: number) { const res await fetchUserDetail(id) currentUser.value res.data } /scriptrefUserInfo | null(null)也是一个很重要的写法。它表示 currentUser 可能是一个 UserInfo也可能是 null。这样在模板里使用currentUser.value.name时TS 会提醒你它可能为空需要你先做判断。虽然写起来多一步但真的能在运行时避免太多“Cannot read properties of null”的崩溃。4.3 抽离通用类型UnwrapRef 和全局类型声明在进阶阶段可以关注一个叫UnwrapRef的内置类型工具函数。它做的事情是把 ref 包着的那层值类型“拆开”。这个在写复杂的泛型组件或者高阶函数时会遇到。举个简单的例子import { ref, type Ref, type UnwrapRef } from vue function createStateT(initial: T): { state: RefUnwrapRefT; setState: (val: T) void } { const state ref(initial) as RefUnwrapRefT const setState (val: T) { state.value val as UnwrapRefT } return { state, setState } }UnwrapRefT的作用是当 T 本身是一个 ref 时取它内部的类型当 T 是普通对象或基本类型时返回它自身。大多数业务代码里不会经常手写这种工具但你在看第三方组件库源码时经常能看到理解它会有助于阅读高质量代码。另外如果你实在有些工具函数、全局变量需要到处用可以在src/types目录下创建一个global.d.ts文件// types/global.d.ts export {} declare global { interface Window { $loading: (show: boolean) void } }这样在任意组件里访问window.$loading都不会报类型错误。但我要提醒一句全局声明是双刃剑用多了会让全局命名空间变得很乱。能局部定义的尽量局部定义。5. 组件通信场景下 TS 的实战配合5.1 父传子props 类型定义带来的正向收益前面我在 3.3 已经详细讲过 defineProps 的类型写法这里我想展开说说它真正解决了我遇到的哪些问题。曾经有一个需求父组件根据用户角色渲染不同权限的按钮子组件是一个按钮组件。一开始用 JS 写props 里定义为type: String父组件传值的时候手滑写成了primary和danger之外的任意字符串运行时只是样式不对React 不会报错界面丑了还得排查半天。改成 TS 之后我用联合类型把可选值限定死script setup langts interface ButtonProps { type?: primary | success | warning | danger | info size?: large | default | small disabled?: boolean } withDefaults(definePropsButtonProps(), { type: default, size: default, disabled: false }) /script这样只要父组件传了一个不在列表里的值编辑器立刻标红有效把“非法值”拦截在开发阶段。这种方式的推进路径其实是一个习惯问题第一次用可能觉得写 interface 麻烦但几次下来你的肌肉记忆会替你决定。5.2 子传父事件参数的类型在 defineEmits 下的安全实践子组件向父组件传值常见场景是表单提交或者列表项操作。TS 对事件的加持主要体现在参数类型上。父组件拿到事件参数后进行后续处理时能正确推导出参数结构不会出现“明明传的是数组回调里当字符串用”的奇葩问题。给一个完整示例!-- Child.vue -- script setup langts interface Product { id: number name: string price: number } interface Emits { (e: select, product: Product): void (e: delete, id: number): void } const emit defineEmitsEmits() const handleSelect (product: Product) { emit(select, product) } const handleDelete (id: number) { emit(delete, id) } /script父组件使用时template Child selectonSelectProduct deleteonDeleteProduct / /template script setup langts import type { Product } from ./types const onSelectProduct (product: Product) { console.log(product.name) } const onDeleteProduct (id: number) { console.log(id) } /script这样父组件的回调函数里product和id的参数类型都是明确的就算以后接口改字段改完 interface两端的代码都能同步感知到变化。5.3 跨级传值provide/inject 的类型定义前面我一直在强调 props 和 emits但 Vue3 里还有一种常用通信方式是provide和inject。这个 API 在处理跨层级共享数据时特别方便但也是最容易类型失控的地方。因为inject默认返回unknown不手动指定的话你在子组件里拿到的数据往上访问属性会直接报错。解决方法是给provide提供明确类型同时给inject一个泛型参数// 父组件 import { provide } from vue import type { InjectionKey } from vue interface ThemeContext { theme: light | dark toggleTheme: () void } export const themeKey: InjectionKeyThemeContext Symbol(theme) provide(themeKey, { theme: light, toggleTheme: () { ... } })子组件里这样取import { inject } from vue import { themeKey } from ./theme const themeContext inject(themeKey) if (!themeContext) { throw new Error(themeContext is not provided) }这里用了InjectionKey它是 Vue3 提供的专门用于 provide/inject 类型推导的辅助类型。用 Symbol 作为 key再把这个 key 的泛型标注好inject 时就能自动推导出类型。为什么 inject 之后还要判空因为inject在没有找到对应的 provide 时会返回 undefined在 TypeScript 的严格模式下这是可能为空的强制判空能避免运行时崩溃。6. 实操中容易遇到的类型报错与排查办法6.1 常见的 TypeScript 报错及解决办法我在写项目的过程中遇到的报错其实高度集中在几个固定模式上。这里整理一个小表方便你快速对照解决报错信息出现场景解决思路Property xxx does not exist on type访问了一个不存在的属性检查 interface 是否漏定义字段或者类型是否声明错误Type string is not assignable to type number给 number 类型的变量赋值字符串检查赋值来源接口字段可能与 interface 不一致Cannot find module ./xxx.vue导入 .vue 文件失败确认 env.d.ts 文件存在确认路径正确Object is possibly null变量可能是 null 或 undefined用可选链?.或者if判空收窄类型Argument of type xxx is not assignable to parameter of type yyy函数参数类型不匹配检查函数定义时的参数类型声明或者调用时的实参结构A function returning void is not assignable to a function returning unknown回调函数返回值类型不一致检查事件回调或高阶函数的类型签名确保返回类型对齐拿Object is possibly null来说这个报错在 ref 初始化为 null 的场景里特别常见。很多新手一看报错就想用as any绕过。我的建议是尽量不用 any而是使用可选链或者显式判空。比如const user refUserInfo | null(null) // 推荐写法判空 if (user.value) { console.log(user.value.name) } // 或者中断后续逻辑 if (!user.value) return一两个地方用 any 可能没事一旦靠 any 解决了十几个报错等于整篇文章的类型安全全废了。6.2 vue-tsc 检查构建前多一道保险Vite 项目默认的构建脚本是vite build但它只做代码打包不做类型检查。也就是说编辑器里如果有些报错被忽略了最终构建出来的代码可能不会报错但类型错误是真实存在的。为了让构建阶段强制检查类型官方推荐在构建前加一个vue-tsc的步骤。在 package.json 里这样修改{ scripts: { build: vue-tsc --noEmit vite build } }vue-tsc --noEmit的意思是只做类型检查不生成编译产物。如果代码里有类型错误构建过程会直接中断。第一次跑的时候很多人会被一堆先前“看不见”的报错吓到但把这些修完之后项目质量确实有了质的提升。我见过一个团队引入 vue-tsc 后一开始全组花了一整天清理历史遗留的类型问题但之后一个月内没有再出现因为字段拼错、传参类型不对而导致的线上 bug。这步非常值得投入时间。6.3 关于 Vue 官方类型工具和未来版本兼容性在 package.json 里你会看到vue-tsc和typescript这两个依赖版本。有同学在安装依赖时遇到过“Vue 类型工具与现有 typescript 不兼容”的提示。这通常是因为 Vue 的一堆类型工具比如vue/runtime-core和某个版本的 TypeScript 存在版本错配。我的建议是在项目里固定主版本号别一有新版本就无缝升级。具体来说创建一个新项目时先用官方模板锁定的版本不要随便升级大版本。如果已经出现了兼容性问题解决办法通常是把两个包一起升级到互相兼容的版本组合。比如我目前用的vue-tsc和typescript版本组合typescript在 5.x 版本时配合vue-tsc的 1.x/2.x 都能正常跑。遇到提示就查一下官方 release notes按它建议的版本组合来。还有一种情况是TypeScript 7.0 中不再支持旧的baseUrl和moduleResolutionnode10写法。如果你的项目里使用的还是旧版 tsp 配置编辑器就会提示选项“baseurl”已弃用并将停止在 typescript 7.0 中运行。这个提示意思不是你的项目马上崩而是告诉你要尽快迁移。实际操作时我给一个经验范围把相对路径里的/别名尽量改成用paths配置去掉baseUrl把 moduleResolution 改成bundler就能兼容未来的 TypeScript 版本。7. 一个完整实战用 TS 封装带类型约束的表格组件7.1 需求梳理和类型设计这一节我想用一个贴近业务的完整例子收尾让你把前面零散的知识点串起来。场景是我在后台管理系统里经常需要的表格组件。它接收列配置、数据源支持 loading 状态并且暴露一个 refresh 方法给父组件。常规的 JS 写法columns 和 dataSource 基本是 any什么都能传组件内部处理时完全靠文档约束。但用 TS 写我们可以把这些都定义得清清楚楚。先定义列配置类型和表格数据类型// types/table.ts export interface TableColumn { key: string title: string width?: number align?: left | center | right slotName?: string } export interface TableData { id: number | string [key: string]: unknown }TableData用索引签名[key: string]: unknown表示表格行数据里可以有很多不确定字段但核心必须有一个 id 字段。这样既有一定的灵活性又能保证每行数据都有唯一标识。7.2 组件实现与类型约束细节表格组件本身不直接依赖业务类型但列配置和数据源的类型是明确的!-- BaseTable.vue -- script setup langts import { ref } from vue import type { TableColumn, TableData } from /types/table interface Props { columns: TableColumn[] dataSource: TableData[] loading?: boolean } const props definePropsProps() const emit defineEmits{ (e: row-click, row: TableData, index: number): void }() const handleRowClick (row: TableData, index: number) { emit(row-click, row, index) } /script template table thead tr th v-forcol in columns :keycol.key :style{ width: col.width ? col.width px : auto } {{ col.title }} /th /tr /thead tbody tr v-for(row, index) in dataSource :keyrow.id clickhandleRowClick(row, index) td v-forcol in columns :keycol.key {{ row[col.key] }} /td /tr /tbody /table /template有一些类型的隐藏知识点row[col.key]的取值TypeScript 是怎么通过的因为TableData有索引签名col.key是 string所以row[col.key]的类型是 unknown而模板插值{{ }}能渲染任何类型所以这里不会报错。但如果你想在自定义插槽里对具体值做精细化操作就需要在使用方把unknown收窄成具体类型。7.3 父组件使用时的类型收益使用方传 dataSource 时可以用一个具体的接口来约束数据行。比如script setup langts import type { TableColumn } from /types/table interface UserRow { id: number name: string age: number status: active | disabled } const columns: TableColumn[] [ { key: name, title: 姓名 }, { key: age, title: 年龄, align: center }, { key: status, title: 状态 } ] const rows: UserRow[] [...] /script template BaseTable :columnscolumns :data-sourcerows row-clickhandleRowClick / /template走到这你会发现虽然 BaseTable 的类型是通用类型但父组件传入的 rows 结构只要不在TableData的约束范围内缺 id就会在编译期立刻暴露。这种“通用组件也能受限”的感觉就是 TS 给组件化作出的最大贡献。8. 我的实操体会与后续可以继续深入的方向如果你之前从没在 Vue3 里写过 TypeScript第一次跑起来很可能觉得报错怎么这么多。我想说这是正常现象而且恰恰说明代码在变好。真实项目中最难的不是写类型而是坚持不破例使用 any。每次想用 any 蒙混过关的时候多问一句“我现在避开的是什么问题”往往能让类型定义再往前走一步。我自己在两个项目里实践完这条路线后最大的体会是类型定义最容易被低估的价值不在写的那一刻而在三周后你回头维护的那一秒。当 IDE 能自动提示出字段名和类型时你对这段代码的理解速度会快很多。后续如果你想继续深入可以关注这几个方向VueUse 这类基于 Composition API 封装的高质量 hooks 库它们全面使用 TypeScript是绝佳的学习素材。Vue Router 和 Pinia 的官方类型定义都做得非常完善可以研究它们的类型推导方式。自己尝试封装一个泛型组件比如虚拟滚动列表、动态表单感受一下类型参数如何流动。最后分享一个我实践中必用的命令在写完一个页面之后跑一次npm run build让vue-tsc帮你把所有隐藏的类型问题都捞出来。第一次可能修报错要花些时间但每修一个你对类型系统的理解就会深一层。这个动作现在已经成为我验证代码的“安全底线”。坦白说用上 TypeScript 之后我再也不愿意回到纯 JS 写大项目的状态了——那种“代码能跑全靠运气”的感觉我确实不想再体会。
返回列表