为什么同样一段类型定义,有人编译时就被拦下,有人却一路绿灯直到线上崩了?差别往往不在水平,而在一个选择:用 interface extends,还是用交叉类型 &。

大多数 TypeScript 类型错误,根源是把这两者当成了可以互换的语法。它们看起来都在"组合类型",但校验发生的时机完全不同——一个在声明时,一个拖到使用时。

打开网易新闻 查看精彩图片

一个当场报错,一个先放你过去

interface 继承会在声明的那一刻做结构校验。子接口一旦和父接口的属性冲突,编译器立刻在定义处报错,构建直接失败,这段类型在问题解决前根本没法被别处使用。

交叉类型则把校验往后推。用 & 组合两个类型时,编译器不会在定义处检查属性是否兼容。冲突的属性类型会被合并成一个联合类型,而不是编译错误。真正的校验,要等到有代码试图给这个类型赋值时才发生。

素材里给了一个很典型的例子:父接口定义 id: string,子接口想把 id 收窄成 number。用 extends,编译器当场拒绝,提示子类型的 id 无法赋值给父类型的同名属性。用 &,同样的定义不报错,直到你构造具体对象时,赋值才失败。

冲突被藏起来,代价在别处付

这个差异在大型代码库里会被放大。一个开发者用 extends 撞上冲突属性,编译器立刻在声明处报错,问题当场暴露。另一个开发者用交叉类型处理同样的场景,什么都没看到,提交代码,类型系统放行了违反约束的值。

"编译器现在就抓住"和"编译器先允许、等有人用错再说",决定了 bug 是在开发阶段冒出来,还是在生产环境炸开。团队往往是在 API 响应被交叉类型标注、却接受了畸形数据、最终拖垮应用逻辑时,才吃到这个亏。

交叉类型对冲突的处理方式值得单独说:原始类型冲突会合并成 never,对象类型冲突则会递归合并属性。这种机制允许结构上的不一致继续存在,只在代码试图满足交叉约束时才浮出水面。

性能账:接口层级会被缓存

除了正确性,还有一笔性能账。TypeScript 编译器会缓存接口层级,所以在大型代码库里,extends 比交叉类型的反复求值更快。

接口继承还强制属性类型只有一个来源,共享模型里不会出现悄悄冒出来的不兼容。交叉类型做不到这一点——它允许不兼容的结构一直待在代码库里,直到有人真的去用它。

在几百个类型的大型应用里,这种延迟校验会让人看不清哪些类型其实已经不可用。开发者往往在离类型定义很远的地方,才第一次撞上不兼容。

选 extends 还是 &,本质上是在选校验发生在哪一刻。声明时拦下,还是使用时才拦下——这个选择,决定了你的类型错误是留在开发机上,还是跟着请求一起上线。