TypeScript中如何基于never类型正确判别联合类型实现类型收窄
问题根源
- 你对
never类型的语义理解有偏差:radius: never表示该属性必须存在,但值不能是任何合法类型,并不等价于「该属性不存在」。你在TCircle和TSquare中都显式声明了必填的radius字段,对TypeScript来说,两个子类型都必然存在radius属性,只是TSquare的radius永远不可能有合法值,这直接导致TSquare本身是无法构造出合法实例的类型。 - 类型收窄逻辑不符合预期:当你用
typeof shape.radius === "number"做判断时,TS会把radius类型为number的TCircle留在if分支,剩下的TSquare进入else分支。此时else分支中shape.radius的类型是never,而TS中never类型代表不可达的代码路径,不会触发类型校验,因此你写的错误运算不会抛错。
解决方案
推荐两种符合TS类型系统设计的实现方式,都能实现你要的类型判别效果:
方式1:用属性存在性判断收窄(最符合直觉)
不要给不存在的字段声明never类型,直接在子类型中只定义自己拥有的字段,用in操作符判断属性是否存在来收窄类型:
type TShape = { id: string; } type TCircle = TShape & { radius: number; } type TSquare = TShape & { size: number; } function getArea(shape: TCircle | TSquare): number { if ('radius' in shape) { // 此处shape自动收窄为TCircle,可正常访问radius return shape.radius * shape.radius * Math.PI; } else { // 此处shape自动收窄为TSquare,访问shape.radius会直接抛编译错误 // 正方形面积正确公式为边长平方 return shape.size * shape.size; } }
方式2:用可选never实现互斥字段(适合必须保留同名字段的场景)
如果你希望两个子类型都保留radius、size字段的声明来实现严格互斥,需要把不属于当前类型的字段标记为可选never(即字段名?: never),而非必填never。可选never表示该字段可以不传,但一旦传值就必须是never(即不允许传任何合法值),既可以实现字段互斥,也能让类型收窄正常工作:
type TShape = { id: string; } type TCircle = TShape & { radius: number; size?: never; // 不允许TCircle存在size属性 } type TSquare = TShape & { radius?: never; // 不允许TSquare存在radius属性 size: number; } function getArea(shape: TCircle | TSquare): number { if (typeof shape.radius === "number") { // 此处shape收窄为TCircle return shape.radius * shape.radius * Math.PI; } else { // 此处shape收窄为TSquare,shape.radius类型为undefined // 写shape.radius * shape.size会触发编译错误,符合预期 return shape.size * shape.size; } }
注意:你原来else分支写的
shape.radius * shape.size本身存在逻辑错误,正方形面积公式应为边长的平方,即shape.size * shape.size,修正类型后TS会帮你捕获这类非法访问错误。
内容的提问来源于stack exchange,提问作者Mario Vernari
相关产品推荐
相关产品推荐

