ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue3+TypeScript练手项目全流程:从零到完整跑通

2026/9/29 13:42:04 拓冰建站 浏览量
Vue3+TypeScript练手项目全流程:从零到完整跑通 上手 Vue3 和 TypeScript最尴尬的处境是什么教程刷了十来遍文档翻了几大页网上 demo 也都跑通了可一旦关掉教程自己想从头搭一个项目立刻不知道该从哪下手。尤其是 TypeScript单独学一遍觉得还行一放进 Vue3 组件里各种类型报错直接把心态搞崩。这篇文章是我用一个 Vue3 TypeScript 练手项目从零到完整跑通的全程记录包括项目该做什么、工具链怎么搭、TS 在组件里到底怎么用、实际开发踩了哪些坑以及练完之后怎么往真实项目升级。内容基本是我实际敲过的代码和踩过的坑适合刚学完 Vue3 基础、想用 TS 完整做一个项目的新手也适合写过几个 demo 但没系统梳理过类型写法的同学参考。1. 练手项目先想清楚做什么才不算白练1.1 一个合格的练手项目要覆盖哪些技术点练手项目最常见的误区是求大求全。我见过有人第一次上手就直接模仿中后台管理系统又是权限模块又是多租户最后写了两个多月还没完成首页。练手项目的核心不是功能多而是把一条正常的业务闭环跑通。什么叫正常业务闭环用户进来能登录、能看到数据列表、能搜索、能新增编辑、能删除退出登录之后路由能拦住未授权访问。这几件事串起来你对 Vue3 组合式 API、TS 类型约束、Pinia 状态流、路由守卫的理解才是用过而不是停留在看过。具体到我这个练手项目功能清单砍到最后就剩六块用户登录、路由守卫、列表页搜索分页、新增编辑表单、删除与详情、全局状态管理。另加一个不算复杂的前置业务场景——我选的是团队待办因为它的数据模型简单只有用户、任务、状态、时间不会像商城那样陷入商品规格、购物车、支付回调的泥潭。这个选择很重要练手项目的业务复杂度必须压到刚好能体现技术要点而不是让业务本身成为学习障碍。我一般建议项目周期控制在三到四周。第一周做环境搭建和登录模块第二周做列表、搜索、分页和表单第三周做状态管理和细节打磨第四周复盘、补注释、尝试加一个你没写过的小功能比如任务拖拽排序或数据导出。按每天两到三小时算这个节奏比较现实。一开始就把范围定死每天都能看见进度这是坚持写完一个练手项目的关键。1.2 技术栈选型Vue3 与 TypeScript 为什么适合组合前端圈一直有 React 和 Vue 的口水战但对练手来说选 Vue3 TS 有几个实打实的好处。第一Vue3 的组合式 API 天然是函数式组织逻辑这正好落在 TypeScript 的强项上。选项式 API 里 this 的类型推断在复杂组件里经常绕弯子而 setup 函数里一切都是变量和函数props、emit、返回值全走签名TS 的推导基本不费劲。我自己从 Vue2 加 class component 转过来之后最大的感受就是原来 TS 和 Vue 可以这么顺。第二Vue3 官方的 defineProps、defineEmits、ref、computed 都对类型做了第一等支持不需要一大堆装饰器或者第三方辅助库。第三生态层面 Element Plus、Naive UI、Vant 这些主流组件库全是 TS 编写类型是第一公民相比之下 Vue2 时代想要在组件库里拿到完整类型很多时候依赖社区补丁坑相当多。这里也要说句实在话如果你连 JavaScript 的数组方法、对象解构、Promise 都还没太搞明白先别急着上 TypeScript。TS 是 JS 的超集不是替代品它的类型标注本身会引入大量新概念基础不稳的时候会加倍痛苦。我遇到过不少新人类型报错其实是业务代码写错了但他以为是 TS 太难。所以推荐顺序是先能熟练写简单 JS 业务再上 Vue3 TS 练手。1.3 项目目录设计一开始就把结构立好很多新手拿到官方脚手架之后第一反应是把所有东西都塞进 App.vue 或者 views 底下的一个大文件里。这个我特别理解因为初期写代码最快。但练手项目一定要从第一天就按功能模块 文件职责来组织目录不然后面加功能的时候会非常痛苦。我用的目录结构是这样的src/ api/ # 接口定义与请求函数 assets/ # 静态资源 components/ # 通用组件 composables/ # 组合式函数 layouts/ # 布局组件 router/ # 路由配置与守卫 stores/ # Pinia 状态 types/ # 全局类型定义 utils/ # 工具函数 views/ # 页面级组件这个结构不是标准答案但它的原则是对的状态放 stores接口放 api类型定义独立放 types通用逻辑抽成 composables页面只负责组装。练手阶段就养成这个习惯等做真实项目的时候会发现收益巨大。反过来如果前期全堆在一起重构成本会让你直接放弃。2. 环境准备与项目初始化把工具链吃透2.1 Node、包管理器与编辑器选择开始之前先把环境校准。Vue3 Vite 项目建议 Node 18 以上我当前用的 Node 20 LTSnpm 10你可以用 nvm 管理多个 Node 版本避免不同项目互相干扰。包管理器方面我推荐 pnpm理由很直接Vite 官方脚手架默认支持 pnpm而且 pnpm 的依赖隔离机制能帮你少踩不少依赖版本错乱的坑。如果你之前只用过 npm也完全可以从 pnpm 开始命令几乎没有学习成本npm install 变成 pnpm installnpm run dev 变成 pnpm dev。编辑器方面自然是 VS Code装两个扩展就够了Volar现在叫 Vue Language Features和 TypeScript Vue Plugin。装完之后需要在 VS Code 里把默认格式化工具设置成 Prettier否则一会儿走 ESLint 一会儿走 Prettier 会很乱。这里有个小坑如果你同时开了 Volar 的 Takeover 模式记得把内置 TypeScript 扩展禁用否则类型提示可能重复或者行为怪怪的。我先给一条验证环境的命令node -v pnpm -v能正常打印版本号就说明基础环境没问题。如果这里就报错大概率是没装 Node 或者环境变量没配好先用搜索引擎查一下对应系统怎么配置环境变量再继续后面的步骤。2.2 create-vue 脚手架每个选项都代表一个决策初始化项目我直接用官方脚手架命令是pnpm create vuelatest为什么不用老的 vue-cliVue3 官方已经把精力全部放到了 Vite 工具链上vue-cli 对 Vue3 的支持基本是维护模式新手没必要学一套即将过时的工具。create-vue 的优点是在交互式命令行里把所有工程化选项问清楚TypeScript、JSX、Router、Pinia、Vitest、ESLint、Prettier、Vue DevTools。很多人一看到这么多选项就全选或者全不选这两个极端都不对。我的建议是TS 必选Router 和 Pinia 选上ESLint 和 Prettier 选上Vitest 如果短期内不会写测试可以先不选JSX 先不选。为什么不选 JSX因为 Vue3 的模板语法和 TS 配合已经足够好JSX 是另一套心智模型练手阶段叠加太多写法反而是负担。注意这里的选择不是终身的后面想加测试随时可以装 vitest但一开始保持精简能让入口不至于太复杂。创建完项目进入目录安装依赖跑起来cd vue3-ts-todo pnpm install pnpm dev浏览器打开 http://localhost:5173能看到 Vue 官方 logo 页面就算成了。这个时候你会注意到一个细节脚手架默认生成的内容里带了一些类型声明的示例文件比如 HelloWorld.vue 里有 defineProps 的类型写法把它读一遍比看十篇文章都有用。2.3 初始化后的目录与工具链分工初始化完成后工程目录大概是这样的我整理成了示意.vscode/ # 编辑器统一配置 src/ assets/ components/ router/ stores/ views/ App.vue main.ts .env.development # 环境变量示例 .gitignore index.html package.json README.md tsconfig.json tsconfig.app.json tsconfig.node.json vite.config.ts这个工程里几个文件的分工要搞清楚入口是 index.html它通过 script 标签加载 src/main.tsmain.ts 负责创建 Vue 实例、挂载 Router 和 PiniaVite 负责开发服务器和打包配置在 vite.config.tsTypeScript 的编译配置拆成了三个 tsconfig其中 tsconfig.app.json 管 src 下的业务代码tsconfig.node.json 管 vite.config.ts 这类构建脚本。这个拆分是官方推荐的不要随便删。我特别提醒一下 tsconfig 里的 paths 配置。很多人初始化之后想用 / 这种别名结果发现不生效因为 vite.config.ts 里的 alias 和 tsconfig.json 里的 paths 必须同时配置而且路径要对应。比如// vite.config.ts resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } } // tsconfig.json 或 tsconfig.app.json { compilerOptions: { baseUrl: ., paths: { /*: [./src/*] } } }不过这里有个新变化我放到后面踩坑章节详细讲因为 TypeScript 新版对 baseUrl 已经提出弃用警告了。3. Vue3 TypeScript 的类型实践不是装饰是护栏3.1 script setup 下的组件类型props、emit 与暴露我在练手项目里最常用的组件写法是script setup langts它会自动把顶层的变量和方法暴露给模板配合 TS 的类型推导非常舒服。先看 props 的类型定义官方推荐直接使用泛型语法script setup langts interface Props { title: string count?: number status: todo | done | doing } const props definePropsProps() // 需要默认值时用 withDefaults withDefaults(definePropsProps(), { count: 0 }) /scriptdefineProps 的泛型写法有编译期检查模板里用到不存在的 prop 会直接报错这比运行时检查前置太多了。defineEmits 同理const emit defineEmits{ (e: update:title, value: string): void (e: delete, id: number): void }()这里给事件参数加了类型父组件监听事件时也能拿到类型提示。defineExpose 则用于子组件向父组件暴露方法配合模板 ref 获取子组件实例的时候类型是完整的不会再像以前那样拿到一个 any。3.2 ref、reactive、computed类型推导与显式标注的边界组合式 API 的三个核心响应式 API 在 TS 下表现不太一样理解它们的推断规则能少写很多多余的标注。ref 接收基础值时会自动推导比如const name ref()得到Refstring但如果初始值是空数组或者空对象你会得到一个不太理想的类型// 不推荐可能被推断成 never[]后面 push 会报错 const list ref([]) // 推荐显式标注泛型 interface TodoItem { id: number title: string done: boolean } const list refTodoItem[]([])reactive 则用在对象类型上它会递归地把所有嵌套属性变成响应式TS 的推导一般可以直接用但如果你需要定义接口还是显式标注更清晰。computed 是三者里最省心的返回值类型会自动推断基本不需要手动写除非你要导出一个复杂类型的纯函数。这里还有一个高频坑当你从接口拿数据返回结构是ApiResponseT但你只需要 data 字段新手经常写const data res.data as any这等于把类型护栏拆了。正确做法是先定义接口interface ApiResponseT { code: number message: string data: T } interface LoginResult { token: string userInfo: UserInfo }然后请求函数返回PromiseApiResponseLoginResult在使用处直接拿到res.data就是LoginResult类型。这个设计能让你从后端数据到页面渲染全链路都有类型这也是我接下来要专门讲接口封装的原因。3.3 接口请求封装里的 TS泛型才是灵魂练手项目里如果每个页面都直接调用 axios你会很快发现两件事一是错误处理代码重复二是类型全靠手写。正确姿势是做一层薄封装把所有请求的返回结构统一约束。我的封装思路是这样// src/utils/request.ts import axios from axios import type { AxiosInstance, AxiosRequestConfig } from axios export interface ApiResponseT unknown { code: number message: string data: T } const service: AxiosInstance axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) service.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( (response) { const res response.data as ApiResponse if (res.code ! 200) { return Promise.reject(new Error(res.message)) } return response }, (error) { return Promise.reject(error) } ) export function requestT(config: AxiosRequestConfig): PromiseT { return service.requestApiResponseT(config).then((response) response.data.data) }然后每个接口文件都走这个 request// src/api/auth.ts import { request } from /utils/request import type { LoginParams, LoginResult } from /types/auth export function login(data: LoginParams) { return requestLoginResult({ url: /auth/login, method: post, data }) }这么做的好处是业务代码里调用 login 后拿到的返回值天然是 LoginResult不需要再手写类型断言也不会因为接口改了字段产生静默错误。3.4 全局类型声明env.d.ts 与 types 目录的边界很多新手会把所有类型都写在 main.ts 或者组件里这会导致类型文件膨胀且难维护。我的习惯是业务实体的类型放在 src/types 目录下按模块拆文件比如 auth.ts、todo.ts面向全局的类型声明放在 d.ts 文件里用 declare global 扩展。比如 Vite 环境变量默认不被 TS 识别你直接写import.meta.env.VITE_API_BASE_URL会报错需要在src/vite-env.d.ts里声明/// reference typesvite/client / interface ImportMetaEnv { readonly VITE_API_BASE_URL: string } interface ImportMeta { readonly env: ImportMetaEnv }还有一类是给后端返回的数据模型声明这部分不建议全用 any哪怕刚开始写不准类型也可以先建一个界面大致约束字段。至于什么时候用 any、什么时候应该精确类型我的原则是数据边界处尽量精确内部临时变量可以用自动推导但不要主动写 any。主动写 any 等于告诉 TS这里不用你管一旦数量多了类型系统就形同虚设。4. 练手项目核心功能实录从登录页到 CRUD4.1 登录页表单校验、调用接口、保存状态到了这个阶段我假设你已经完成了环境和类型基础接下来就真正开始写业务。登录页虽然简单但它把表单、接口、状态管理、路由跳转几个环节串起来了非常适合作为第一个完整功能。我的登录页结构是模板部分放表单script setup 部分负责校验和提交逻辑。校验可以用 Element Plus 自带的 Form 规则也可以用简单的 if 判断练手阶段重要的是提交函数里的类型和状态流转const loginForm reactiveLoginForm({ username: , password: }) const loading ref(false) async function handleLogin() { if (!loginForm.username || !loginForm.password) { ElMessage.warning(请输入用户名和密码) return } loading.value true try { const res await loginApi(loginForm) userStore.setToken(res.token) userStore.setUserInfo(res.userInfo) router.push(/) } catch (e) { // 统一错误提示已经在请求拦截器里做了 } finally { loading.value false } }这里有一个很重要的细节登录成功后 token 是放到 Pinia 还是 localStorage我的答案是都放——Pinia 管应用内状态localStorage 做持久化。刷新页面后在入口重新从 localStorage 恢复 token。如果你只放内存一刷新登录态就没了路由守卫立刻把你弹回登录页。4.2 路由守卫未登录拦截与动态元信息路由守卫是登录功能的另一半。练手项目至少要有全局前置守卫逻辑是没有 token、目标页需要认证就跳转登录页有 token 但访问的是登录页就跳回首页。注意这里不要每次都从 localStorage 读 token最好封装一个工具函数或者在 store 里写 getter。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.path /login token) { next({ path: / }) } else { next() } })我建议把 redirect 参数用上登录成功后再跳回来这对用户体验很重要。后面如果做权限系统再扩展到按角色动态生成路由练手阶段先不要碰动态路由那个复杂度不是新手该承受的。4.3 列表页搜索、分页、loading 状态机列表页是练手项目中代码量最大的部分因为要处理状态、请求、渲染三层。我需要强调一个我犯过很多次的错误把 loading 只当成一个 boolean结果接口报错的时候整个页面没有反馈。更好的做法是把请求状态设计成idle | loading | success | error这样一个状态机或者至少区分 loading 和 error。搜索参数和分页参数建议用一个响应式对象统一管理const query reactive({ keyword: , status: as | todo | done, page: 1, pageSize: 10, total: 0 }) async function fetchList() { loading.value true try { const res await getTodoList({ keyword: query.keyword, status: query.status || undefined, page: query.page, pageSize: query.pageSize }) list.value res.items query.total res.total } finally { loading.value false } }搜索按钮和分页组件的事件都调用 fetchList注意分页时要把 page 和 pageSize 回写进 query。这里还有一个容易漏掉的细节切换分页大小时应该把 page 重置为 1否则你在第 5 页切成每页 50 条接口可能会返回空列表。这个逻辑虽然小但很影响体验。4.4 表单页v-model 类型、回显与提交表单页的核心是 v-model 和类型对齐。Element Plus 的表单组件接收的 value 类型和你提交给接口的类型不一定完全一致比如日期选择器返回的是数组但你接口需要的是字符串范围。练手项目我建议在表单页维护一个独立的表单模型interface TodoFormModel { title: string description: string priority: low | medium | high dueDate: string | null }编辑场景还有回显问题从列表页跳过来时带着 id需要先请求详情再填充表单。这个时候要注意时序页面 onMounted 里是异步的模板不能一开始就访问 form.dueDate否则会报错。最简单的方法是加一个 loaded 标志数据回来后再渲染表单组件。很多新手遇到编辑页空白就是这个问题。const loaded ref(false) const form reactiveTodoFormModel({ ... }) onMounted(async () { const id route.params.id if (id) { const data await getTodoDetail(Number(id)) Object.assign(form, data) } loaded.value true })提交函数则复用登录页的 loading 模式成功后跳回列表并刷新。到这里一个完整的登录加 CRUD 闭环就跑通了。5. 新手必踩的坑我整理了一份排查清单5.1 TypeScript 版本升级带来的弃用警告如果你用的是最新版 create-vue并且 TypeScript 已经到 5.5 或更高可能会在终端看到两条黄色警告选项 baseUrl 已弃用并将停止在 TypeScript 7.0 中运行以及选项 moduleResolutionnode10 已弃用并将停止在 TypeScript 7.0 中运行。这不是你的代码有问题而是老 tsconfig 写法正处在版本过渡期。应对方法分两步。第一moduleResolution 改成 bundler因为 Vite 项目本身走 esbuild 打包用 node10 这种老式解析方式是历史包袱。第二去掉 baseUrl直接依赖 paths 配合相对路径。我最后稳定下来的 tsconfig 核心配置长这样{ 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, baseUrl: ., paths: { /*: [./src/*] } } }如果你仍然看到警告先看一下是不是 create-vue 生成的配置模板还没更新可以手动把 moduleResolution 调成 bundler 试试。注意只有打包工具真正支持 bundler 语义才能这样改Vite 项目完全没问题。5.2 类型断言能不用就不用但数据边界例外我在练手项目里看到最多的类型问题就是乱用 as any。比如从 localStorage 读出 JSON 再转对象新手习惯写JSON.parse(str) as any后面的所有代码都失守了。我的建议是数据边界处定义好接口让类型错误提早暴露。比如interface StoredUser { userId: number name: string avatar: string role: string } function getUserFromStorage(): StoredUser | null { const raw localStorage.getItem(user) if (!raw) return null try { return JSON.parse(raw) as StoredUser } catch { return null } }这里的 as StoredUser 是合理的因为 JSON.parse 在 TS 里只能返回 any边界处收窄一次是必要的。但不要在业务代码里到处 as尤其是把类型 A 直接断言成类型 B那几乎都是在掩盖设计问题。5.3 响应式丢失、组件不更新与时序问题练手阶段大家喜欢把接口请求直接写在 setup 顶层期望页面加载就发请求。但 setup 执行时组件还没挂载有些 DOM 相关操作会报错。正确做法是把请求放进 onMounted或者在 setup 顶层请求但不要立即访问 DOM 相关引用。另一个非常典型的坑是响应式丢失。从 reactive 对象或 Pinia store 里直接解构出来的属性在 Vue3 里不会保持响应式。很多人写了const { userInfo } userStore然后页面怎么都不更新。正确做法是// 错误解构后丢失响应式 const { userInfo } userStore // 正确用 storeToRefs 包裹 const { userInfo } storeToRefs(userStore)或者直接用userStore.userInfo这也是响应式的。组件里如果用了 reactive 对象同样不要直接解构必要的时候用 toRefs 转换。还有一类组件不更新问题常见于文件上传组件的成功回调。比如你配置了 on-success 但始终监听不到这通常不是组件库的 bug而是你的请求拦截器把响应数据结构改了成功回调拿到的参数结构和组件库预期不一致。排查思路是打日志看回调参数到底是什么而不是怀疑组件库有问题。5.4 第三方类型缺失与组件库类型导入练手项目一般都会引入一个组件库比如 Element Plus。正常来说它的类型是齐全的但偶尔你会遇到类型上不存在属性 xxx这样的报错。这时候先检查是不是组件库版本和 Vue 版本不匹配再看是不是导入路径写错了。Element Plus 的全局类型可以通过在 d.ts 里配置实现按需引入不需要手动导入每个组件的类型。另外一个常见困扰某个老包没有提供 TypeScript 类型定义import 进来直接 any 报错。处理方式是在项目里新建一个 d.ts 文件用 declare module 补上类型declare module some-js-lib { export function init(options: { debug?: boolean }): void }这属于接口隔离的思路我不建议直接 suppress 或 ignore因为你后面升级依赖时没有类型会很难受。5.5 问题排查速查表我把练手阶段最常见的几类问题汇总成一张表方便你对照排查现象可能原因处理办法终端出现 baseUrl 弃用警告TypeScript 5.5 开始标记旧配置移除 baseUrl只保留 paths并改成相对路径终端出现 moduleResolutionnode10 弃用警告老模板默认配置改成 node16 / nodenextVite 项目用 bundler模板里读取不到 props没有用 defineProps 声明检查是否在 script setup 中声明了 props 类型调用接口返回值没有类型提示请求函数缺少泛型用 request 泛型包装定义响应接口解构 store 后页面不更新响应式状态被抽离使用 storeToRefs 或直接访问 store 属性编辑页回显空白异步回填时序问题加 loaded 标志数据返回后再渲染表单数组 push 报错ref 初始值为空数组导致 never[]显式标注 refTodoItem[]([])on-success 回调不到请求拦截器改了响应结构打印回调参数调整与组件库的契约这张表不是万能药但它覆盖了新手练手项目里频率最高的几个点。遇到问题先对号入座能省下不少排查时间。6. 练手完成后下一步往哪走6.1 从练手项目到真实项目的差距在哪里很多新人练完一个项目觉得下一步就是接外包或者投简历实际上中间还有不小的距离。练手项目的特点是需求明确、接口自造、没有团队协作而真实项目至少要面对几个新问题多人合并代码冲突、接口联调时的字段变动、产品修改需求导致的组件设计变化、旧浏览器兼容、性能优化、错误监控。这些能力不是靠写一个 demo 能获得的但练手阶段养成的好习惯——类型完整、目录清晰、代码可读——恰恰是解决这些真实问题的底层能力。如果你想更接近真实项目可以把练手项目扩展加一个 mock server 模拟真实接口延迟和失败加一个单元测试覆盖核心逻辑把它部署到服务器上用一个真实域名访问。这个过程会迫使你考虑环境变量、跨域、静态资源部署这些在本地开发环境完全不会出现。6.2 值得继续深入的方向测试、工程化与组合式函数练手项目跑通之后我个人强烈建议先补 Vitest 单元测试因为测试能反过来强迫你把代码拆得更合理。一个组件如果很难写测试往往说明它做的事情太多这是个很好的重构信号。其次是写自己的组合式函数比如 useAuth、usePagination把之前写在组件里的逻辑抽出来你会发现组件代码一下子瘦身很多而且对 Vue3 的逻辑复用会有更深的体会。再往后可以接触 VueUse 这个社区组合式函数库看看别人怎么设计通用 hooks但不要照抄要理解每个函数背后的响应式原理。最后才是 SSR、微前端这些大工程方向练手阶段不需要碰。6.3 说点我自己的真实体会最后分享一点个人经验。我用 Vue3 TS 练手的时候最大的瓶颈不是找不到资料而是资料太多太散今天看这个教程明天看那个 demo结果一周下来一个完整功能都没写出来。后来我给自己定了一条规矩一个练手项目只固定一套技术方案遇到坑就查这一套方案的官方文档不看其他分支方案的比较文章。事实证明这个做法非常高效因为 Vue3 TS 的方案在官方文档里足够完整你缺的只是把时间花在写代码上。写这个项目的过程中还有一个体会类型报错不可怕可怕的是不敢开 strict 模式。我一开始也是把严格模式关掉来绕过报错后来发现那些报错恰恰是代码质量的提示。把 strict 打开把每个报错都当作一次学习机会练完一个项目之后你会发现自己对 TS 的理解上了一层。这个练手项目的代码我后来又重构过两遍每次都有新收获希望你能比我更快地走到能用自己的项目去验证想法那一步。