先记住这个答案
interface extends 表达一个接口满足基础接口的契约,继承时的冲突通常会直接报错;交叉类型 A & B 要求同时满足两边,即使组合语法成立,同名属性也可能变成无法构造的 never。接口适合有稳定成员、明确扩展关系的公开契约,交叉便于组合已有类型表达式。两者都不会提供运行时继承或复制实现。需要改变某字段含义时,先检查是否真是兼容扩展;如果是替换,应明确移除旧键再定义,避免制造隐藏冲突。
- 接口扩展会检查继承契约是否相容
- 交叉可以延后暴露无法满足的字段约束
- 类型组合不会复用任何运行时方法实现
接口继承为什么尽早拒绝冲突
基础接口声明标识是字符串,下游就可能直接调用字符串方法。扩展接口如果改成数字,就无法再作为基础接口使用,因此继承声明应当失败。多个基础接口存在不一致成员时也可能在扩展处报错,帮助作者定位冲突来源。
交叉别名则可以写出同时要求字符串与数字的标识。这不表示编译器接受了任意一种标识,而是字段被压缩成没有正常值的类型。示例保留这种差异,并展示显式替换字段的写法,让改变契约的意图出现在声明处。
interface Entity { id: string }
// @ts-expect-error 数字 id 不满足 Entity 的字符串契约
interface NumericEntity extends Entity { id: number }
type Conflicted = Entity & { id: number };
type ImpossibleId = Conflicted['id'];
// @ts-expect-error id 需要同时满足 string 和 number
const invalid: Conflicted = { id: 1 };
type Replaced = Omit<Entity, 'id'> & { id: number };
const replaced: Replaced = { id: 1 };继承冲突在声明处被拒绝,交叉别名本身可定义,但数字对象不能构造它。Replaced 明确删除旧 id 后再定义数字字段,因此它是一份新契约而非原接口的兼容扩展。
为什么不能随意继承联合类型
普通接口需要能够静态确定成员的对象类型作为继承基础。若基础是两个不同对象的联合,成员到底属于哪个分支并不固定,直接 extends 这样的联合会受到限制。需要保留候选分支时,通常继续使用类型别名组织联合与交叉。
类型别名还可以命名原始值、元组和条件类型等表达式,接口则围绕对象契约并支持声明合并。选择不必变成风格争论:先看需要表达的类型是否适用,再考虑公开扩展、错误定位和团队维护方式,避免为了统一写法扭曲模型。
组合很多层后怎样定位错误
看到某属性成为 never,可以沿交叉成员逐个查找该键的来源,检查是否有字符串与数字、互斥字面量等冲突。不要立即用断言构造值,否则下一层调用仍可能要求不可满足的条件。必要时把复杂表达式拆成有意义的中间别名。
公开配置类型增加一个交叉成员也可能使既有调用者突然无法赋值,尤其是插件类型碰巧使用了同名键。评审时应检查实际字段重叠和代表性调用,不能只验证类型声明文件本身能编译;错误常常发生在消费端构造对象的地方。
容易答错的地方
- 交叉没有在声明处报错就代表组合合理
- 它可能把冲突保留为 never 属性,直到创建对象或使用接口时才暴露;应验证真实调用样例能否满足组合后的契约。
- extends 会自动继承方法实现
- 接口只存在于类型层,声明的方法不会生成可运行的函数;复用实现需要类继承、对象组合或普通函数等运行时机制。
面试官还会怎么问?
同名字段缩小为合法子类型可以吗?
兼容的缩小可能满足基础契约,例如把字符串字段收窄为特定字面量;仍应检查写入语义和可变别名是否符合实际使用预期。
Omit 后重定义还可以当原接口使用吗?
只有替换后的字段继续满足原契约时才可以;把字符串改为数字就不再兼容,使用 Omit 不会自动建立这种替换的安全性。
为什么有时整个交叉会变成 never?
编译器会对某些互斥判别字段进行整体归约,具体结果需要结合成员形状查看;不能把所有同名字段冲突都概括为相同表现。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。