当“兼容”不等于“相等”:从 TypeScript 到相关性的方向性思维
Hatched by Nan Wang
Aug 26, 2026
2 min read
0 views
92%
一个对象只要拥有你需要的字段,就可以被当作另一种类型使用。两个变量只要呈现出稳定关系,就可能被认为彼此相关。
但这两句话都隐藏着一个危险问题:谁在判断兼容,兼容是为了什么,关系又朝哪个方向成立?
TypeScript 的结构类型系统与现代相关性统计,看起来属于两个毫不相干的世界。一个讨论程序中的对象能否传入函数,另一个讨论数据中的变量是否相关。然而它们共同揭示了一个经常被忽略的事实:相似、可替代、相关、可预测和等价,并不是同一件事。
如果不能区分这些概念,我们就会在代码中错误地接受对象,在数据中错误地解释关系,最后把“能用”误认为“就是它”,把“有关联”误认为“可以互换”。
结构相同,为什么不代表意义相同
TypeScript 的一个核心思想是结构类型兼容。假设有如下类型:
type User = {
name: string;
age: number;
};
const employee = {
name: "Lin",
age: 32,
department: "Engineering"
};
const user: User = employee;
employee 多出了 department 字段,但它仍然可以赋值给 User。原因很简单:从 User 的使用者角度看,所需结构都已经存在。函数只需要读取 name 和 age,并不在意对象内部是否还有其他内容。
这是一种非常强大的工程原则:**判断一个对象能否被使用,不必追问它来自哪里,只需检查它是否提供了当前任务所需要的结构。**它降低了耦合,让代码更容易复用,也让不同模块能够通过共同的接口协作。
可是,结构兼容只回答了一个有限问题:这个值能否满足某个接口的操作要求。它没有回答这个对象在现实世界中究竟是什么,也没有回答它是否与另一个对象具有相同的业务含义。
例如:
type Meters = number;
type Seconds = number;
在 TypeScript 看来,米和秒都只是 number。一个接受米数的函数,理论上可以收到秒数,因为底层结构完全一致。但这并不意味着两者在语义上可以互换。程序通过了类型检查,现实却可能已经被悄悄写错。
这正是结构类型的边界:结构是可见的,意图往往不可见。
同样的边界,也出现在统计学中。
相关性不是镜子,而是一种观察方式
最熟悉的皮尔逊相关系数,主要衡量两个变量之间的线性关系。若一个变量增加时,另一个变量大致按固定比例增加,相关系数会较高。斯皮尔曼的 rho 和肯德尔的 tau 则更适合识别单调关系,也就是一个变量增加时,另一个变量总体上持续增加或持续减少,即使这种关系并不呈直线形状。
这已经告诉我们:关系的定义依赖于观察者选择的尺度。
如果我们只看原始数值,可能得到一种结论。如果把数据转换成排名,再看顺序关系,结论可能不同。排名方法不关心变量具体是十、二十还是一千,而关心谁排在谁前面。它抛弃了一部分数值信息,却获得了对异常值和非线性单调关系的更强韧性。
近年来出现的新的依赖性度量进一步挑战了“相关性必须对称”的直觉。传统相关系数通常满足:变量 X 与 Y 的相关程度,等于 Y 与 X 的相关程度。但某些新的系数可以设计成具有方向性,例如用一个变量的排序结构去考察另一个变量是否能够被解释或预测。此时,X 对 Y 的依赖程度不一定等于 Y 对 X 的依赖程度。
这并不违反数学,而是说明我们问的问题变了。
“X 和 Y 是否线性相关?”是一个较为对称的问题。
“知道 X 之后,我能否理解或预测 Y?”则是一个带方向的问题。
一张城市地图可以帮助你从地址找到街区,但从街区反推出唯一地址就困难得多。地图本身没有改变,改变的是任务方向。数据关系也是如此:同一组观测可以支持对称的相似性判断,也可以支持不对称的预测性判断。
真正的共同点:兼容性与相关性都取决于“任务”
TypeScript 结构类型和方向性相关性之间最深的连接,不是它们都在讨论“关系”,而是它们都拒绝一种过于简单的想法:关系是对象或变量自身固定携带的属性。
一个对象是否兼容,取决于你把它传给什么函数,以及函数需要哪些字段。一个变量是否有用,取决于你要预测什么、使用什么度量、保留哪些信息,以及是否关心方向。
可以把这种思想概括成一个三层模型:
第一层:形状
对象有哪些字段,变量有哪些数值,数据呈现什么分布。这是最容易观察的一层。
在代码中,形状可能是 name、age 和 email。在统计中,形状可能是散点图、排序结果,或者一组均值与方差。
第二层:操作
你准备对它做什么。
代码可能只是读取一个字段,也可能调用一个方法、修改状态或序列化对象。统计分析可能要测量线性关系、单调关系,或者评估一个变量对另一个变量的预测能力。
第三层:意义
这个对象在业务中代表什么,这个变量在现实机制中扮演什么角色。它是否是原因、结果、代理变量、选择偏差的产物,或者只是共同受到第三个因素影响。
许多错误都来自用第一层替代后两层。我们看到两个对象字段相同,就认为它们可以互换。我们看到两个变量相关,就认为一个能够解释另一个,甚至认为它是原因。
结构决定你能否开始操作,任务决定你是否应该这样操作,意义决定操作结果是否值得相信。
这套三层模型可以同时改善软件设计与数据分析。它提醒我们:类型检查和相关性检验都非常有价值,但它们通常只是在为更深层的问题提供入口,而不是替我们完成判断。
一个程序员和数据科学家都会遇到的陷阱
设想一个电商系统中存在两个对象:Customer 和 Supplier。它们都有 name、email 和 address。从结构上看,二者可能兼容某个简单的 Contact 接口:
type Contact = {
name: string;
email: string;
address: string;
};
如果函数只负责发送通知,这种兼容很合理。但如果函数名叫 issueRefund,它还接受一个看似相同结构的对象,那么问题就变了。退款对象需要的不只是字段,还包括身份权限、订单关系和资金责任。
同一结构,在一个任务中是充分条件,在另一个任务中却远远不够。
数据分析中也有完全对应的情形。假设冰淇淋销量与溺水事故数量在夏季同时上升。二者可能具有明显相关性,但这并不意味着冰淇淋导致溺水。更合理的解释是,气温和户外活动同时影响了两者。
如果进一步观察方向性,销量或许能帮助预测某些溺水风险,因为它间接反映了天气和人流。但反过来,知道溺水事故数量未必能同样准确地预测冰淇淋销量。预测上的不对称,不能直接变成因果上的证明。
这里至少有三种不同的关系:
- 结构相似:两者共享某些可观察属性。
- 统计依赖:知道一个变量后,另一个变量的信息不再完全不可得。
- 因果作用:改变一个因素,会系统性地改变另一个结果。
第一种关系适合支持接口复用,第二种关系适合支持预测和特征选择,第三种关系才适合指导干预。把它们混在一起,就会产生“类型上能传入,所以业务上正确”或“统计上相关,所以干预有效”的错误。
从对称思维转向方向性思维
人类很容易偏好对称结构。相等是对称的,传统相关性是对称的,许多接口命名也暗示着双方可以互换。但现实中的系统往往是有方向的。
函数向对象索取能力,而不是向对象索取完整身份。预测模型从输入产生输出,而不是自动允许输出反推输入。权限系统允许某个角色执行某个动作,也不意味着动作拥有同等程度的反向权限。知识图谱中的边也有方向:购买者购买商品,不能因为商品被购买,就说商品购买了购买者。
这带来一个非常实用的分析工具:在面对任何“关系”时,先补全一句话:
谁对谁,在什么任务中,以什么标准,具有怎样的关系?
例如,不要只说“销售额和广告投入相关”,而要问:
“在保持季节、地区和预算周期可比的情况下,广告投入能多大程度预测未来销售额?”
不要只说“这个对象符合用户类型”,而要问:
“它是否满足这个函数的全部操作要求?函数是否会把它当作用户身份使用?”
不要只说“这两个指标高度相关”,而要问:
“这种关系是线性的、单调的,还是仅仅来自共同趋势?它是否具有方向性?如果改变其中一个变量,另一个变量会不会真的变化?”
问题一旦被这样重新表述,许多争论会自动变得清晰。因为我们不再争论一个模糊的“它们是不是一样”,而是在明确比较不同任务下的可用性、可预测性和可替代性。
一个可立即使用的“关系审计”框架
无论是在写代码、设计 API,还是分析数据,都可以进行四步关系审计。
一,确认表面结构
列出你真正拥有的字段、属性、排序或观测值。不要先加入解释,也不要把名称当成意义。
代码中检查必需字段、可选字段和方法签名。数据中检查变量类型、缺失值、异常值,以及分析是否依赖原始数值或排名。
二,明确实际操作
问清楚系统接下来要做什么。是读取、比较、修改、授权,还是预测、排序、筛选和干预?
如果操作不同,所谓的兼容性也应该分开定义。用于展示的 Contact 不应自动获得用于退款的 Customer 权限。适合排序的单调依赖,也不应被包装成因果模型。
三,检查方向和信息损失
关系是否真的可以双向使用?从 X 到 Y 的预测能力,是否等于从 Y 到 X?从完整对象到简化接口的信息通常是有损的,因此通过接口使用对象,并不意味着可以从接口还原完整对象。
同样,把数值转换为排名会丢失距离信息。排名关系对异常值可能更稳健,但它无法回答那些依赖具体差值的问题。
四,分离“可用”与“可信”
类型系统可以告诉你某个值满足静态接口,统计指标可以告诉你样本中存在某种关系。二者都不能独立保证业务含义正确或现实机制成立。
因此,在关键系统中应加入语义约束。可以使用品牌类型、封装构造函数、领域对象和运行时验证,防止米与秒被当成同一个数字。数据分析则需要控制混杂因素、验证时间顺序、进行外部检验,并明确指标到底服务于预测还是干预。
Key Takeaways
- 把兼容性写完整:不要只问两个对象结构是否相同,要说明它们在哪个函数、哪个权限边界和哪个业务任务中兼容。
- 把相关性写完整:说明关系是线性的、单调的还是更一般的依赖,并注明是否具有方向性。
- 不要把可替代性当成等价性:一个对象可以满足接口,却不代表它拥有相同的业务意义;一个变量可以预测另一个变量,也不代表它是原因。
- 警惕信息损失:额外字段被忽略、数值被转换成排名、复杂身份被压缩成简单接口时,都要检查丢失的信息是否正是决策所需要的部分。
- 在重要边界加入语义验证:静态类型和统计指标负责降低一类错误,领域规则、实验设计和人工审查负责处理它们无法看见的错误。
结语:最危险的不是不相似,而是相似得刚刚好
完全不同的东西通常不会造成误判。真正危险的是那些在表面上足够相似,因此能够通过第一轮检查,却在方向、用途或意义上悄悄不同的东西。
一个普通数字可能是价格、距离、时间或权限等级。一个共享字段的对象可能是用户、供应商或攻击者伪造的身份。一种稳定的统计关系可能适合预测,却完全不适合干预。
因此,成熟的工程和分析能力,不是不断寻找更多“相似性”,而是学会判断相似性在哪个边界内有效。
真正可靠的系统,不会只问“它们像不像”,而会继续追问“像到什么程度,朝哪个方向,为了什么任务,以及代价是什么”。
当我们用这种方式看待类型、数据和现实世界时,“兼容”不再是一个简单的肯定或否定。它变成了一份带有上下文、方向和责任的契约。
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣