先记住这个答案
在 strictFunctionTypes 开启时,普通函数类型和函数属性的参数按更严格的兼容规则检查:只能处理点击事件的函数不能替代需要处理任意事件的回调,因为调用者可能传入没有坐标的事件。方法和构造器声明的参数存在历史兼容例外,可能保留双向兼容行为,所以同样形状换成方法语法后赋值结果可能不同。这不是因为函数使用了泛型。设计回调接口时应优先表达真实可接受输入,不要通过改成方法语法绕过原本有意义的错误。
- 回调必须能处理调用方承诺的所有输入
- 严格函数参数检查对方法声明存在例外
- 泛型本身不意味着参数默认双向兼容
为什么更具体的处理器反而不能替代
通用事件只保证类型标识,点击事件还包含坐标。只能处理点击事件的函数可能直接读取坐标,因此不能放到允许调用者传入任意通用事件的位置。输入端的安全方向与返回更具体数据的输出端方向不同,面试中应通过实际调用解释。
示例分别声明函数属性接口和方法接口。严格模式拒绝把点击专用属性当成通用事件处理属性,而方法语法保留兼容例外。示例只演示赋值差异,不实际调用那个可能不安全的方法,避免把编译通过误当作行为已经正确。
type EventBase = { kind: string };
type ClickEvent = EventBase & { x: number };
type Handler<T> = { handle: (event: T) => void };
interface Method<T> { handle(event: T): void }
const clicks: Handler<ClickEvent> = { handle: event => { event.x.toFixed(); } };
// @ts-expect-error 通用调用方可能传入没有 x 的事件
const allEvents: Handler<EventBase> = clicks;
const clickMethod: Method<ClickEvent> = { handle(event) { event.x.toFixed(); } };
const compatibleMethod: Method<EventBase> = clickMethod;函数属性赋值应产生预期诊断,方法赋值则在此配置下被接受。后者仍可能被传入缺少 x 的事件,因此不要把语法例外理解为编译器证明了专用处理器可以处理所有事件。
方法例外为何需要明确讲出来
TypeScript 在既有 JavaScript 类层次和泛型容器兼容性上有现实取舍,因此严格选项并未把所有参数位置完全统一。两个接口看起来都叫 handle,冒号后的函数类型与方法声明却可能走不同规则,阅读第三方类型时尤其需要注意。
不能因此给所有回调都改成方法语法,让报错消失。错误可能准确指出消费者承诺过宽、处理器能力过窄。修复应调整事件模型、缩小订阅范围或提供能处理全部候选的回调,让运行时职责与签名一致。
与泛型、参数数量和返回值分开讨论
泛型只是让同一种接口按不同 T 实例化,参数兼容规则即使没有泛型也同样存在。允许回调忽略额外参数、返回值兼容以及方法参数双向兼容是几个不同维度,不应全部概括成函数参数永远协变或永远逆变。
验收事件 API 时,除了查看赋值是否通过,还要用不同合法事件调用处理器,确认实现没有假定额外字段。特别是包装库或类型技巧主动放宽了参数关系时,类型系统的保护范围会减少,运行契约需要更清楚地记录和测试。
容易答错的地方
- 泛型函数参数默认都是双向协变
- 参数检查取决于编译选项与函数或方法等声明位置,泛型并不是触发双向兼容的通用原因;应使用具体赋值与调用关系解释。
- 改成方法语法通过后就没有安全问题
- 方法兼容例外可能接受实际无法处理全部输入的实现,编译通过不能保证运行行为;应修正真实事件范围或处理能力。
面试官还会怎么问?
能处理所有事件的函数可以处理点击事件吗?
通常可以,因为点击事件也满足通用事件契约,通用处理器不会要求调用方额外提供点击专有字段;这正是输入能力方向的核心。
关闭 strictFunctionTypes 会怎样?
普通函数参数的部分检查会放宽,某些严格模式拒绝的赋值可能通过;这减少了静态发现输入不匹配的机会,不会修复运行时问题。
接口方法和类方法都应该禁止吗?
不需要,方法有正常的对象行为建模用途;关键是知道参数兼容例外,在回调或消费者接口中按真实输入承诺选择合适声明。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。