TypeScript 协变和逆变怎么理解
在前端开发里,“类型匹配”是很多人容易忽略但又常踩坑的细节——比如你想把一个“猫”的实例,传给需要“动物”的函数,代码能不能跑?如果反过来,把“动物”实例传给要“猫”的函数,又会不会报错?这其实就是和TypeScript里的两个核心概念:协变、逆变,直接相关。很多人刚学TS时,只会记“类型要对上”,但真遇到传参、赋值的报错,就摸不着头脑,其实用生活化的例子,就能把这两个概念讲透。
先搞懂最基础的“父子类型”
要理解协变和逆变,得先记住一个前提:你得先有“类型的大小关系”——就像“猫”是“动物”的一种,“猫”的范围比“动物”小,是“动物”的子类型;反过来,“动物”是父类型。简单说,范围小的是子类型,范围大的是父类型。打个比方:“文具”是父类型,“钢笔”是子类型,因为钢笔属于文具的一种,钢笔的范围更小。

什么是协变?
协变就是“子类型能替换父类型”,换成人话就是:“更小范围的东西,可以传给更大范围的需求”。举个生活化的例子:你要一个能装文具的收纳盒(需求是“文具”,父类型),你拿个装钢笔的小盒子(钢笔,子类型),完全没问题——小盒子能装的东西更少,肯定满足“装文具”的要求,甚至还能多放橡皮、尺子,绰绰有余。
放到TS代码里就更清楚了:比如我们定义两个类型,Cat是Animal的子类型,然后函数feedAnimal的参数要传Animal,但你传Cat的实例,TS不会报错,这就是协变。这也是为什么平时写代码时,把具体的小类型传给通用的大类型,是最常用也最安全的操作,不用特意做什么额外处理。
什么是逆变?
逆变刚好和协变相反,是“父类型能替换子类型”——也就是“更大范围的东西,可以传给更小范围的需求”。这里得注意,这种操作不像协变那么“理所当然”,得看场景,不然就会出问题。还是用刚才的收纳盒例子:你要一个只能装钢笔的小收纳盒(需求是“钢笔”,子类型),结果你拿了个能装文具的大盒子(文具,父类型),这时候就有风险了:大盒子里可能装了铅笔、本子,甚至是剪刀,你把它当成钢笔盒用,万一不小心从里面拿出铅笔,就不是你要的“钢笔”了。
对应到TS里,逆变常见的场景是函数参数。比如你定义一个函数,要求参数是“处理钢笔的函数”,但你传了一个“处理文具的函数”,TS大概率会报错——因为处理文具的函数可能会碰到铅笔、本子,而处理钢笔的函数只懂钢笔,万一真把铅笔传进去,就会出错。这也是为什么平时写代码时,不要随便把通用的函数传给需要更具体函数的地方,不然很容易踩坑。
为什么要懂协变和逆变?
很多人觉得,平时写代码只要不写错就行,为什么要纠结协变和逆变?其实这两个概念是TS类型安全的核心——协变保证了代码的通用性,让你可以用具体的小类型适配通用的大类型,不用每次都写一模一样的通用代码;而逆变保证了代码的安全性,避免你把范围太大的东西,用到需要范围很小的地方,不小心引入隐藏的bug。
举个实际的开发例子:你要做一个宠物喂养的系统,需要一个“喂动物”的函数,又需要一个“喂猫”的函数——喂猫的函数参数是猫,而喂动物的参数是动物,这时候把喂猫的函数传给需要喂动物的场景,TS允许(协变),这很合理,因为喂猫的函数肯定能喂动物;但如果反过来,把喂动物的函数传给需要喂猫的场景,TS就会阻止(逆变的安全机制),因为喂动物的函数可能会喂狗,传到喂猫的地方就会出错。
这样一来,既不会限制你写通用的代码,又能帮你提前发现隐藏的类型问题,不用等到运行时才报错。其实不用把协变和逆变想成多么复杂的术语,只要记住“小的换大的叫协变(安全),大的换小的叫逆变(要谨慎)”,平时写代码时避开逆变的坑,就能很好地利用TS的类型系统了。