先看个最简单的例子:接口赋值。TypeScript检查类型兼容性根本不关心具体类型名,只看结构是否匹配。比如你定义了一个Person接口,要求有name和age字段,这时候哪怕你拿一个Dog类型的对象过来,只要它恰好也有同名字段且类型匹配,TypeScript就会放行。这就是著名的"鸭子类型"------走起来像鸭子,叫起来像鸭子,那就是鸭子。
但这里有个细节要注意,源类型必须包含目标类型的所有必需属性。反过来就不行了:
函数兼容性就更有意思了。函数参数检查遵循"逆变"原则------听起来很玄乎,其实很简单:参数类型可以比定义的更宽泛,但不能更严格。比如下面这个例子:
为什么func2赋值给func1不行?因为func2定义时承诺能处理string和number,但func1只能处理number,如果把func2交给func1,碰到string参数就崩了。
返回值则相反,遵循"协变"------返回值类型必须更具体或完全相同:
数组和元组的兼容规则经常让人栽跟头。普通数组看类型参数是否兼容:
但元组就严格多了,必须长度和每个位置的类型都匹配:
枚举类型比较特殊,数字枚举和number互相兼容,但字符串枚举谁也不认:
类的兼容性规则有点反直觉。只比较实例成员,静态成员和构造函数不参与比较。而且private和protected成员会影响兼容性:
泛型兼容性最容易让人困惑。泛型本身不参与兼容性判断,最终看的是具体类型:
any类型在兼容性检查中是万能牌,但也很危险。unknown只能赋值给any和unknown自身,而never可以赋值给任何类型。
实际开发中,掌握这些规则能帮你避免很多奇怪的错误。特别是在使用第三方库时,经常需要自定义类型声明,理解兼容性规则就能写出更健壮的类型定义。记住TypeScript的类型系统是结构化的,这点和很多传统静态语言完全不同。
最后给个实用建议:当遇到复杂的类型兼容问题时,可以先把鼠标悬停在变量上查看推断出的类型,然后用类型断言临时绕过检查。但长期来说,还是应该理顺类型关系,毕竟类型安全的本质是为了减少运行时错误,而不是和编译器斗智斗勇。