类型缩小与控制流分析很多人第一次学联合类型时都会产生一种挫败感。不是不会写string | number而是写完之后发现“怎么什么都不能用了”。这其实不是 TypeScript 在刁难你而是类型系统在提醒一件很重要的事你已经承认这个值有多种可能那在真正使用它之前就必须先确认当前到底是哪一种。这就是类型缩小存在的意义。它不是额外附加的一套技巧而是联合类型真正能落地使用的前提。什么是类型缩小类型缩小可以用一句话概括通过代码中的判断把一个宽泛类型收窄为当前分支里更具体的类型。functionprintId(id:string|number){if(typeofidstring){console.log(id.toUpperCase());}else{console.log(id.toFixed(2));}}这里的id在函数入口处是string | number但进入if分支后它被缩小成了string进入else分支后它被缩小成了number。这就是 TypeScript 非常核心的一种工作方式它不只是看类型声明本身也会结合你的控制流分支去推断当前上下文里到底还能剩下哪些可能。为什么没有类型缩小联合类型几乎没法用假设你写functionhandle(value:string|number){returnvalue.toUpperCase();}这段代码报错不是因为toUpperCase()不好而是因为number分支不具备这个方法。联合类型的代价就在这里只要某个能力不是所有成员共有的你就必须先确认当前成员。也正因如此联合类型越重要类型缩小就越重要。现实项目中你会发现这两者几乎是成对出现的。最常见的缩小方式typeof适合基础类型判断functionformatValue(value:string|number|boolean){if(typeofvaluestring){returnvalue.trim();}if(typeofvaluenumber){returnvalue.toFixed(2);}returnvalue?true:false;}typeof简单直接是很多基础分支判断的第一选择。instanceof适合类实例判断functionprintError(error:Error|string){if(errorinstanceofError){console.log(error.message);}else{console.log(error);}}只要你在处理类实例、异常对象、自定义类instanceof都很常见。in适合对象结构判断typeDog{bark:()void};typeCat{meow:()void};functionspeak(animal:Dog|Cat){if(barkinanimal){animal.bark();}else{animal.meow();}}当联合类型的差异体现在属性结构上时in非常好用。真值判断也会触发缩小但要小心语义functionprintLength(value?:string){if(!value)return;console.log(value.length);}这里的if (!value) return会让后面的value自动排除undefined和空字符串等假值可能。TypeScript 会参与这种控制流分析。但要注意真值判断虽然方便却可能混淆“空字符串”和“未定义”这两种业务语义不同的情况。比如一个表单输入为空字符串与一个字段根本不存在未必是一回事。所以工程上最好问自己我现在是想排除所有假值还是只想排除undefined/null判别联合是最值得掌握的模式如果你只记住类型缩小的一种工程写法那应该是判别联合。它的思路非常简单给联合类型中的每个分支都加一个稳定且唯一的判别字段。typeSuccessResponse{status:success;data:string[];};typeErrorResponse{status:error;message:string;};typeApiResponseSuccessResponse|ErrorResponse;functionhandleResponse(response:ApiResponse){if(response.statussuccess){console.log(response.data);}else{console.log(response.message);}}这里的status就是判别字段。它有几个明显优势可读性极强分支逻辑自然非常适合接口响应、组件状态、任务状态和业务语义高度一致一旦你开始写前端状态机、请求状态、表单提交流程、任务调度这种模式几乎无处不在。用kind、type、status这类字段做判别通常比“猜结构”更稳很多人喜欢通过“某个字段在不在”去缩小类型例如用in判断message或data。这可以用但长期来看不如专门设计判别字段稳定。原因很简单结构可能演化字段可能重名某些分支可能后来也出现这个字段如果你从一开始就设计一个明确的判别字段类型和业务都会更清楚。用户自定义类型守卫有时候判断逻辑会重复出现。这时可以把它封装成类型守卫函数typeAdmin{role:admin;permissions:string[]};typeMember{role:member;points:number};functionisAdmin(user:Admin|Member):userisAdmin{returnuser.roleadmin;}有了这个函数之后functionprintUserInfo(user:Admin|Member){if(isAdmin(user)){console.log(user.permissions);}else{console.log(user.points);}}这种写法的价值在于它不只是复用逻辑也把“这个判断一旦成立类型应该如何收窄”一起复用了。控制流分析TypeScript 会跟着你的程序路径走TypeScript 的强大不只是看单个if判断而是会跟踪更长的控制流。functionprintLength(value?:string){if(!value){return;}console.log(value.length);}这里return之后TypeScript 会认为剩下分支中的value一定已经被过滤过了。这就是控制流分析。类似的还有提前throwswitch分支多层条件判断逻辑运算后的结果它本质上意味着TypeScript 不是静止地看类型而是在模拟代码路径。穷尽检查是类型缩小的高级实践当你有一个判别联合并且希望确保未来新增分支时不会漏处理可以用never做穷尽检查typeShape|{kind:circle;radius:number}|{kind:rectangle;width:number;height:number};functiongetArea(shape:Shape){switch(shape.kind){casecircle:returnMath.PI*shape.radius*shape.radius;caserectangle:returnshape.width*shape.height;default:{const_exhaustiveCheck:nevershape;return_exhaustiveCheck;}}}如果以后你新增triangle却忘了在switch中处理never检查就会直接提示你。这个技巧在大型项目里非常实用。一个常见误区我明明知道它是什么为什么还报错这是初学者最常说的一句话。答案很简单你“脑子里知道”没有用TypeScript 只相信代码里被明确表达出来的事实。类型系统不会读心也不会根据你的业务背景自动猜测。它要的是可以验证的条件。你想让它信服就必须把判断逻辑写出来。工程建议别把类型缩小看成麻烦它其实在逼你把分支说清楚很多人把类型缩小当作额外工作量。实际上它往往是在暴露原本就存在但被忽略的业务分支。比如一个接口到底可能成功还是失败一个字段到底可能为空还是缺失一个对象到底是管理员还是普通成员这些分支本来就存在。TypeScript 只是逼你承认并处理它们。本文小结类型缩小是连接“联合类型”和“可执行逻辑”的桥梁。联合类型负责表达可能性类型缩小负责在分支里确认现实。你真正掌握 TypeScript不是因为你会写A | B而是因为你知道在代码路径中如何把这个联合安全地收窄成一个可操作的具体形态。如果说联合类型让你开始描述复杂状态那么类型缩小则让这些状态真正进入程序流程。它是 TypeScript 走向工程实用性的第一道门槛。练习写一个函数接收string | string[]分别输出长度并比较typeof与Array.isArray的使用方式。设计一个带kind字段的图形联合类型包含圆形、矩形和三角形并根据kind计算面积。写一个类型守卫函数isSuccessResponse让它能帮助你缩小接口返回类型。后记2026年5月21日于上海。