in / out 协变与逆变

最后更新:2026-10-22

in / out 协变与逆变

型变描述"复杂类型里的类型参数如何影响整体兼容性":返回值协变(子类型可顶替)、参数逆变(接受父类型的函数能顶替)。函数赋值兼容、数组/接口的嵌套兼容都由它决定——理解了它,"为什么这个赋值报错/不报错"就有了系统答案。

分组泛型
类型语法
严格模式不需要

ℹ️ 说明 编辑器将 TypeScript 转译为 JavaScript 后运行(不做类型检查);类型检查结果见示例 Output。

📝 语法

// TS 4.7 起可显式声明型变(默认由编译器结构化推断)
interface Producer<out T> { produce(): T; }        // 协变:只产出 T
interface Consumer<in T> { consume(v: T): void; }   // 逆变:只消费 T

// 函数赋值的天然规则
let handleDog: (d: Dog) => void = handleAnimal;          // OK:参数逆变
const adoptAnimal: (tag: string) => Animal = adoptDog;   // OK:返回值协变

⚙️ 参数

<out T>T 只出现在输出位置(返回值):Producer<Dog> 可当 Producer<Animal> 用
<in T>T 只出现在输入位置(参数):Consumer<Animal> 可当 Consumer<Dog> 用
<in out T>读写都开放:必须严格同型,否则不兼容

返回值:型变是类型兼容性规则,不产生运行时代码;它决定的是"谁可以赋给谁"。

▶ 示例

修改下面的代码,点击试一试查看转译运行结果:

输出:

Rex eats food
Rex-A1 eats food

(转译后运行输出;反向不成立——把只处理 Dog 的函数赋给 (a: Animal) => void 槽位会被编译器拒绝,这正是参数逆变在拦下不安全的赋值)

💡 要点

ℹ️ 说明 直觉记忆:"产出可以更具体(协变),消费可以更宽泛(逆变)"——养宠人(接受 Animal)当然能伺候 Dog,狗舍(产出 Dog)当然能供应 Animal。
⚠️ 注意 可变数组既产出又消费(双变),是不安全赋值的重灾区;写泛型接口时先想清楚 T 是进还是出,再标 in/out。
💡 提示 TS 4.7 的显式注解主要是文档与加速:结构推断大多正确,但标注 in/out 能让深层类型检查更快、意图更明确、错误信息更直白。
更多案例即将上线。

📚 相关教程

❓ 常见问题

Q 协变和逆变一句话区分?
A 看类型参数出现在哪:输出位置(返回值)协变——子类型整体兼容;输入位置(参数)逆变——父类型整体兼容。既是输入又是输出则不变(严格同型)。
Q 为什么函数参数是逆变不是协变?
A 反例说明:若 (d: Dog) => void 能赋给 (a: Animal) => void,调用方传 Cat 就炸了。而 (a: Animal) => void 赋给 (d: Dog) => void 安全——它能处理任何 Animal,Dog 当然在内。
Q 什么时候需要手写 in/out 注解?
A 类型参数位置复杂、结构推断慢或结果反直觉时(如事件 emitter、深嵌套接口)。日常业务代码依赖默认结构推断即可,不必处处标注。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏