TypeScript 泛型协变怎么用

平时写TypeScript代码,你有没有遇到过这样的场景?比如你写了一个函数,专门用来处理「猫」类型的数组,后来想让它也能处理「宠物」类型的数组,结果编译器直接报错,连修改的地方都不知道从哪下手。其实这背后就藏着今天要讲的「泛型协变」——它不是什么高高在上的概念,只是帮你解决这类“类型兼容”问题的实用工具。

先搞懂:什么是“类型兼容”?

先不说泛型,咱们用生活里的例子理解最清楚。假设你有一个“宠物投喂器”,本来只能放猫粮(对应猫类型),后来你想给它放狗粮(对应狗类型),但狗粮和猫粮都是“宠物食物”(对应父类型),理论上能兼容吧?但TypeScript里默认的规则,就像这个投喂器只认“猫粮”,不认“宠物食物”,哪怕狗粮也属于宠物食物。

TypeScript 泛型协变怎么用

泛型协变,就是给这个投喂器加一个“通用接口”,让它不仅能放猫粮,还能放狗粮这类所有宠物食物——核心就是让「子类型的泛型」能兼容「父类型的泛型」。

泛型协变:用例子一步看懂

拿代码说事儿,咱们写个最简单的泛型函数:

`typescript

// 先定义父类型和子类型

interface Pet { name: string }

interface Cat extends Pet { meow: () => void }

interface Dog extends Pet { bark: () => void }

// 普通泛型函数,参数是T类型的数组

function feedPets<T>(pets: T[]) {

pets.forEach(p => console.log(喂${p.name}))

}

`

现在,Cat是Pet的子类型,那Cat[]是不是Pet[]的子类型?按TypeScript默认规则,Cat[]和Pet[]是完全不兼容的,你这么写会直接报错:

`typescript

const myCats: Cat[] = [{ name: "咪咪", meow: () => {} }]

feedPets<Pet>(myCats) // 编译器会红波浪线:类型不兼容

`

这时候就需要泛型协变出马。怎么实现?在泛型参数前加out关键字,这是TypeScript专门给协变留的语法:

`typescript

// 加了out,这个泛型参数就是协变的

function feedPets<out T>(pets: T[]) {

pets.forEach(p => console.log(喂${p.name}))

}

`

现在再试刚才的代码,编译器就不报错了!因为加了out后,Cat[]就成了Pet[]的兼容子类型,相当于投喂器终于能装所有宠物食物了。

什么时候用?别瞎加!

很多人会说“那我所有泛型都加out不就完了?”这可不行,得记住一个核心规则:out只能用在「只作为输出」的泛型参数里。

啥叫“只作为输出”?比如刚才的feedPets函数,只拿pets读name属性,完全没往数组里塞任何东西,这就是只输出(读)的情况,加out没问题。

反过来,如果函数既要读又要写,就不能加out。比如写一个给宠物加名字的函数,往数组里塞新的宠物:

`typescript

// 这里泛型参数T既要读(name)又要写(push),不能加out

function addPet<T>(pets: T[], newPet: T) {

pets.push(newPet)

}

`

要是硬加out,编译器会直接报错——因为如果加了out,就不能修改泛型参数里的内容,不然会出问题。

总结:协变是“懒人的类型兼容小工具”

其实泛型协变本质就是帮你减少重复代码,不用给每个子类型都写一遍函数。比如你要喂猫、喂狗、喂所有宠物,只要一个加了out的泛型函数就能搞定,不用重复写三个不同的投喂函数。

下次你遇到“明明逻辑上兼容,但TypeScript说类型不对”的问题,不妨想想是不是泛型参数的协变规则没考虑到——只要记得out加在只读的泛型参数上,就能轻松解决大部分兼容问题,不用再硬转类型、绕弯子改代码。

上一篇 Java try catch finally 怎么用