什么网站可以做试卷,潍坊仿站定制模板建站,云建站不能用了吗,网站制作技术培训学校先说个真事上个月组里来了个新人#xff0c;工作两年#xff0c;简历上写着精通TypeScript。第一天他就跟我说#xff1a;TypeScript就是JavaScript加个类型标注#xff0c;有啥难的#xff1f;我笑了笑没说话。一周后#xff0c;他提交的代码炸…先说个真事上个月组里来了个新人工作两年简历上写着精通TypeScript。第一天他就跟我说TypeScript就是JavaScript加个类型标注有啥难的我笑了笑没说话。一周后他提交的代码炸了。测试同学过来找他为什么这个订单的金额显示NaN他自己看了半天代码说不应该啊我都写了类型的。我过去看了一眼function calculateTotal(order: any) { return order.items.reduce((sum, item) sum item.price, 0); }看出问题了吗order.items可能是undefined直接崩了。你不是写了类型吗 测试同学问。我写了啊你看order: any... 他突然意识到了什么。对any就是我放弃治疗的意思。编译器看到any就不管了你写啥都行能不能跑就看运气了。这就是我想说的90%的人以为自己在用TypeScript其实只是在用带类型注释的JavaScript。今天我们就聊聊那些真正能救命的TypeScript技巧——每个都是我或者我同事踩过坑之后的血泪经验。一、类型定义到处复制你需要知道类型也能用下标我遇到的问题去年做一个用户中心项目后端定义了用户的数据结构。前端各个组件都需要用到用户的某些字段于是我就到处复制类型定义// 用户的完整信息 interface User { id: string; name: string; email: string; profile: { age: number; bio: string; avatar: string; address: { province: string; city: string; street: string; } } } // 个人资料组件需要profile interface UserProfile { age: number; bio: string; avatar: string; address: { province: string; city: string; street: string; } } // 地址组件需要address interface UserAddress { province: string; city: string; street: string; }看起来没啥问题对吧直到有一天后端说我们给address加了个zipCode字段。我我得去找所有用到address的地方一个个改。改了十几个文件漏了两个又出bug了。后来学到的方法其实根本不用复制可以直接索引出来interface User { id: string; name: string; email: string; profile: { age: number; bio: string; avatar: string; address: { province: string; city: string; street: string; } } } // 直接用方括号取出类型就像访问对象属性一样 type UserProfile User[profile]; type UserAddress User[profile][address]; type City User[profile][address][city]; // 甚至可以一直取下去这就是 Indexed Access Types索引访问类型。听起来高大上其实就是类型也可以像对象一样用方括号访问。现在后端改了address我只需要改User这一个地方其他地方自动就同步了。为什么有用单一数据源改一处生效不会因为复制粘贴导致类型不一致重构的时候省心编译器会告诉你哪里不对二、别用any了学会自己教TypeScript识别类型踩过的坑以前写代码遇到不确定类型的数据我就直接anyfunction processData(data: any) { console.log(data.name); // 能跑但可能炸 }能跑是能跑但data到底是个啥有没有name这个属性天知道。有一次接了个后端接口返回的数据可能是用户信息也可能是错误信息const response await fetch(/api/user); const data await response.json(); // data的类型是any // 我直接用了 console.log(data.name); // 如果是错误信息这里就炸了更好的办法Type Guard类型守卫后来我学会了先检查一下// 定义一个检查函数 function isUser(data: any): data is User { return ( data typeof data object typeof data.id string typeof data.name string ); } // 使用的时候先检查 const response await fetch(/api/user); const data await response.json(); if (isUser(data)) { // 在这个if里面TypeScript知道data是User类型 console.log(data.name); // ✅ 安全 console.log(data.email); // ✅ 有提示 } else { console.error(数据格式不对); }关键在于data is User这个写法。这是在告诉TypeScript如果这个函数返回true那data就是User类型。听起来有点绕换个说法你在教TypeScript一个判断规则——怎么知道一个东西是User。之前TypeScript不知道怎么判断现在你教它了它就能在代码里自动推导类型了。真实收益上个月重构了一个老项目把所有any改成了Type Guard发现了17个潜在的bug全是可能访问undefined的属性这种问题。三、再也不怕漏掉case强制你处理所有情况遇到的问题写一个订单状态处理函数type OrderStatus pending | paid | cancelled; function getStatusText(status: OrderStatus) { switch (status) { casepending: return待支付; casepaid: return已支付; casecancelled: return已取消; } }看起来完美对吧三个状态都处理了。某天产品经理说加个退款中状态吧。后端改了type OrderStatus pending | paid | cancelled | refunding;然后你的代码编译照样通过因为语法上没问题。结果上线后用户点了退款页面啥反应都没有——因为你忘了处理refunding。正确的做法Exhaustive Checking加一行代码就能避免function getStatusText(status: OrderStatus) { switch (status) { casepending: return待支付; casepaid: return已支付; casecancelled: return已取消; default: // 关键在这里 const _exhaustive: never status; return _exhaustive; } }现在如果你加了refunding但忘了处理编译就报错Type refunding is not assignable to type never.为什么有用如果所有case都处理了走到default的时候status的类型就是never永远不会走到这里如果漏了某个case那个值就会跑到default类型对不上就报错用人话说你在default放了个绝对不会执行的代码如果真的执行了说明你漏了东西。我现在写枚举处理都会加这一行救了我好多次。四、既要类型检查又要保留具体值用satisfies遇到的矛盾做主题系统的时候有个配置对象const theme { primary: #1890ff, success: #52c41a, error: #ff4d4f };现在有两个需求要类型检查确保所有value都是string要保留具体值theme.primary的类型是#1890ff而不是string为什么要保留具体值因为后面要根据颜色值生成深浅色变体如果类型是string就推导不出来了。以前的办法都不完美// 方法1加类型注解 type Theme Recordstring, string; const theme: Theme { primary: #1890ff, // theme.primary的类型变成了string丢失了具体值 success: #52c41a, error: #ff4d4f }; // 方法2用as const const theme { primary: #1890ff, // theme.primary的类型是#1890ff但... success: #52c41a, error: #ff4d4f } asconst; // 整个对象变readonly了不能改了完美解决satisfiesTypeScript 4.9加的新功能type Theme Recordstring, string; const theme { primary: #1890ff, success: #52c41a, error: #ff4d4f } satisfies Theme; // 完美 // ✅ 编译器会检查是否符合Theme类型 // ✅ theme.primary的类型是 #1890ff保留了具体值 // ✅ theme不是readonly可以修改satisfies的意思就是这个对象满足Theme类型但不要把它变成Theme类型。听起来绕简单说就是既要检查又不要改变原来的类型。五、从类型里提取信息神奇的infer实际场景项目里有很多API函数async function getUserInfo(id: string): PromiseUserInfo { // ... } async function getOrderList(page: number): PromiseOrderList { // ... }现在问题来了我在其他地方想用UserInfo这个类型但我不想重复定义怎么从函数里提取出来用infer自动推导// 定义一个工具类型 type GetReturnTypeT T extends (...args: any) Promiseinfer R ? R : never; // 使用 type UserInfo GetReturnTypetypeof getUserInfo; // 自动推导出UserInfo type OrderList GetReturnTypetypeof getOrderList; // 自动推导出OrderListinfer是什么意思简单说就是我不知道这里是什么类型TypeScript你帮我推导一下推导出来的结果叫R。再举个例子提取Promise的返回值type UnwrapPromiseT T extends Promiseinfer R ? R : T; type A UnwrapPromisePromisestring; // A是string type B UnwrapPromisePromisenumber; // B是number type C UnwrapPromiseboolean; // C是boolean不是Promise原样返回执行过程以Promisestring为例TypeScript看到T extends Promiseinfer R发现T确实是Promise...的形式推导出里面的类型是string把它赋给R返回R也就是string更实用的例子提取数组元素类型type ArrayElementT T extends (infer R)[] ? R : never; type Numbers ArrayElementnumber[]; // Numbers是number type Strings ArrayElementstring[]; // Strings是string上周我用这个重构了整个API类型系统原来要手写的几十个类型定义现在都自动推导了。六、函数参数太多容易搞混给元组加标签踩过的坑以前定义一个坐标类型type Point [number, number]; function drawLine(start: Point, end: Point) { const [x1, y1] start; const [x2, y2] end; // ... } drawLine([0, 0], [100, 50]); // 哪个是x哪个是y搞不清问题是调用的时候[0, 0]到底哪个是x哪个是y记不住啊。尤其是参数多了type Rect [number, number, number, number]; function drawRect(rect: Rect) { // rect是 [x, y, width, height] 还是 [left, top, right, bottom] // 鬼知道 }Labeled Tuples带标签的元组TypeScript 4.0加的功能type Point [x: number, y: number]; function drawLine(start: Point, end: Point) { // ... } // 现在鼠标悬停的时候编辑器会显示 // start: [x: number, y: number]更明显的例子type Rect [x: number, y: number, width: number, height: number]; function drawRect(rect: Rect) { const [x, y, width, height] rect; // 一目了然 }好处不会搞混参数顺序编辑器有更好的提示代码更容易理解上个月重构地图系统的时候把所有坐标类型都加了标签新人上手快了很多。七、批量生成类型Mapped Types遇到的需求做表单系统每个字段都需要验证函数type FormData { username: string; email: string; password: string; age: number; }; // 需要为每个字段定义验证函数 type FormValidators { username: (value: string) boolean; email: (value: string) boolean; password: (value: string) boolean; age: (value: number) boolean; };手写30个字段写到吐血。Mapped Types自动生成type ValidatorsT { [K in keyof T]: (value: T[K]) boolean }; type FormValidators ValidatorsFormData; // 自动生成了怎么理解keyof T拿到所有keyusername | email | password | age[K in keyof T]遍历每个keyT[K]拿到这个key对应的类型组装成(value: T[K]) boolean再举个例子生成所有字段都可选的类型type MyPartialT { [K in keyof T]?: T[K] }; type User { id: string; name: string; email: string; }; type PartialUser MyPartialUser; // 相当于 // { // id?: string; // name?: string; // email?: string; // }更酷的修改key的名字type GettersT { [K in keyof T asget${CapitalizeK string}]: () T[K] }; type User { name: string; age: number; }; type UserGetters GettersUser; // 结果 // { // getName: () string; // getAge: () number; // }上周用这个给200多个数据模型自动生成了getter方法的类型定义爽歪歪。八、强制命名规范Template Literal Types真实场景组里规定所有事件处理函数必须以handle开头。但总有人忘记代码review的时候很烦。后来我用类型强制了type EventHandler handle${Capitalizestring}; function registerHandler(name: EventHandler, handler: Function) { // ... } registerHandler(handleClick, () {}); // ✅ 通过 registerHandler(handleSubmit, () {}); // ✅ 通过 registerHandler(onClick, () {}); // ❌ 编译报错 registerHandler(click, () {}); // ❌ 编译报错现在谁要是不按规范写编译都过不了。更实用的API路由校验type ApiRoute /api/${string}; function callApi(url: ApiRoute) { fetch(url); } callApi(/api/users); // ✅ callApi(/api/orders/123); // ✅ callApi(/users); // ❌ 编译报错必须以/api/开头CSS类名规范BEMtype BEMClass ${string}__${string} | ${string}__${string}--${string}; function addClass(className: BEMClass) { // ... } addClass(button__icon); // ✅ block__element addClass(button__icon--active); // ✅ block__element--modifier addClass(button-icon); // ❌ 不符合BEM规范上个月用这个规范了整个组件库的类名**CSS命名相关的bug少了90%**。九、过滤类型Distributive Conditional Types需求场景有个联合类型想过滤掉null和undefinedtype MixedType string | number | null | undefined | boolean; // 想要得到string | number | boolean用分配条件类型type NonNullableT T extends null | undefined ? never : T; type CleanType NonNullableMixedType; // 结果string | number | boolean为什么叫分配因为TypeScript会自动把联合类型拆开一个个处理执行过程 1. string extends null | undefined ? never : string → string 2. number extends null | undefined ? never : number → number 3. null extends null | undefined ? never : null → never 4. undefined extends null | undefined ? never : undefined → never 5. boolean extends null | undefined ? never : boolean → boolean 最后把结果合并string | number | boolean更实用的提取某种类型type ExtractStringT T extends string ? T : never; type Mixed string | number | boolean | string; type OnlyStrings ExtractStringMixed; // string过滤掉函数类型type NonFunctionT T extends Function ? never : T; type Mixed string | number | (() void) | boolean; type NoFunctions NonFunctionMixed; // string | number | boolean这个在处理复杂数据结构的时候特别有用。十、配置对象别乱改as const和readonly血泪教训以前有个全局配置const config { apiUrl: https://api.example.com, timeout: 5000, retries: 3 };某天调试的时候我改了timeoutconfig.timeout 1000; // 调试用然后忘了改回来直接提交了。结果线上所有请求超时时间变成1秒大量接口超时。用as const锁死const config { apiUrl: https://api.example.com, timeout: 5000, retries: 3 } as const; config.timeout 1000; // ❌ 编译报错Cannot assign to timeout because it is a read-only property现在谁都改不了想改也得先去掉as const不会不小心改了。另一个好处保留字面量类型const directions [north, south, east, west] as const; type Direction typeof directions[number]; // Direction north | south | east | west // 不是 string路由配置的例子const routes [ { path: /home, component: Home }, { path: /about, component: About }, { path: /contact, component: Contact } ] asconst; type RoutePath typeof routes[number][path]; // RoutePath /home | /about | /contact function navigate(path: RoutePath) { // 只能用定义过的路由 } navigate(/home); // ✅ navigate(/profile); // ❌ 编译报错现在所有配置文件我都用as const再也不担心被人误改了。十一、处理嵌套数据Recursive Types递归类型遇到的问题要定义JSON数据的类型但JSON可以无限嵌套const data { name: John, age: 30, friends: [ { name: Jane, age: 28, friends: [ { name: Bob, // 可以一直嵌套下去... } ] } ] };怎么定义类型递归类型定义type Json | string | number | boolean | null | Json[] // 数组里可以是Json | { [key: string]: Json }; // 对象的值也可以是Json const data: Json { name: John, age: 30, friends: [ { name: Jane, age: 28, friends: [/* 无限嵌套类型都对 */] } ] };关键在于Json的定义里引用了自己就像递归函数一样。树形菜单的例子type MenuItem { id: string; label: string; icon?: string; children?: MenuItem[]; // 递归children也是MenuItem数组 }; const menu: MenuItem { id: 1, label: 系统设置, children: [ { id: 1-1, label: 用户管理, children: [ { id: 1-1-1, label: 添加用户 }, { id: 1-1-2, label: 用户列表 } ] }, { id: 1-2, label: 权限管理 } ] };评论回复的例子type Comment { id: string; content: string; author: string; replies?: Comment[]; // 评论下面可以有回复回复下面还能有回复 }; const comments: Comment { id: 1, content: 这篇文章写得不错, author: User1, replies: [ { id: 2, content: 确实, author: User2, replies: [ { id: 3, content: 1, author: User3 } ] } ] };上个月做组织架构树的时候用递归类型定义无论多少层级都能完美支持。最后说说我的感受以前我也觉得TypeScript麻烦——写个类型定义比写业务代码还费劲编译报错一堆改半天有这时间我代码都写完了但现在回过头看那些麻烦的类型定义帮我避免了无数次线上事故。上周重构一个老项目把核心模块加了完善的类型定义编译器直接帮我找出了23个潜在bug——全是可能访问undefined、遗漏case处理这种问题。如果没有TypeScript这些bug只能等用户遇到了再来报。TypeScript的价值不是让你写得更快而是让你睡得更安稳。不用担心改了一个地方其他地方炸了不用担心遗漏了某个状态处理不用担心配置被人乱改。尤其是大型项目、多人协作的时候TypeScript就是你的安全网。快速回顾技术点解决什么问题记住这句话Indexed Access Types类型定义到处复制类型也能用方括号访问Type Guardsany满天飞不安全教TypeScript怎么识别类型Exhaustive Checking忘记处理某个case让编译器强制你处理所有情况satisfies既要检查又要保留字面量满足类型但不变成那个类型infer从类型中提取信息让TypeScript帮你推导Labeled Tuples元组参数容易搞混给元组的每个位置加标签Mapped Types批量生成类型遍历key生成新类型Template Literal Types强制命名规范字符串也能有类型约束Distributive Conditional Types过滤联合类型自动拆开联合类型处理as const readonly防止配置被改锁死数据保留字面量Recursive Types处理嵌套结构类型定义里引用自己你的项目里用到这些技巧了吗或者遇到过哪些本可以用TypeScript避免的坑评论区聊聊。觉得有用的话点个赞、转给你的同事让更多人少踩坑、少加班。关注「前端达人」每周分享实用的前端开发经验帮你写出更好维护、更少bug的代码。下期见