前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库TypeScript 泛型方差 推断 in out
TyTypeScript泛型设计

TypeScript 怎样根据泛型参数的位置判断协变与逆变,in 和 out 需要手写吗?

方差讨论的是换一组类型参数后,整个泛型实例的兼容方向如何变化。

前端进阶之旅 · 一题精讲更新于 2026.09.06
TypeScript#泛型设计#类型系统
先看核心答案读代码示例
理解线索

沿泛型实例的使用方向思考

  1. 生产者输出具体值时能满足较基础的读取期待
  2. 消费者处理更广输入的函数能够服务较具体调用者
  3. 结构比较实际成员及其位置决定能否安全替代

函数属性与方法声明的参数规则可能不同,推理时必须同时查看实际语法和编译配置。

核心回答

先记住这个答案

TypeScript 会依据泛型结构中类型参数的使用方式推断方差。只产生 T 的接口通常体现协变,消费 T 的函数属性通常体现逆变;同时读写可能形成更严格关系,方法语法又有兼容例外。方差推断与一次调用中推断 T 是什么不是同一个问题。in、out 等注解用于少数确有需要的类型设计与诊断场景,不能用来任意改写结构类型行为;它们只在相应的实例化比较中发挥作用,不应被当成强制所有比较按某种方向运行的开关。

  • 先区分产生值与消费值对应的使用方向
  • 方差关系不同于本次调用选择哪个 T
  • 通常依赖结构推断,不随意添加 in/out 注解

为什么生产与消费方向相反

能够生产点击事件的对象,输出包含通用事件需要的全部字段,因此可以服务只需要通用事件的使用者。能够消费任意通用事件的函数,也能接收点击事件;反过来,只会消费点击事件的函数却不能保证处理其他事件。

示例用函数属性明确消费位置,避免方法兼容例外混入基础方向。两个安全赋值分别展示生产者协变与消费者逆变,错误赋值则保留预期诊断。解释时沿实际读取与调用路径走一遍,比背诵输入逆变输出协变更容易看出原因。

生产者与消费者的安全替代方向TypeScript
type EventBase = { kind: string };
type ClickEvent = EventBase & { x: number };
type Producer<T> = { make: () => T };
type Consumer<T> = { consume: (value: T) => void };
const clicks: Producer<ClickEvent> = { make: () => ({ kind: 'click', x: 1 }) };
const events: Producer<EventBase> = clicks;
const acceptAny: Consumer<EventBase> = { consume: value => { void value.kind; } };
const acceptClick: Consumer<ClickEvent> = acceptAny;
// @ts-expect-error 只能消费点击事件的接口不能承诺消费所有事件
const unsafe: Consumer<EventBase> = acceptClick;

生产点击事件可以满足基础事件读取,通用消费者可以服务点击调用方。最后的静态赋值按目标接口承诺检查,即使某个运行对象碰巧来自更宽消费者,也不能把窄契约任意扩张。

同时出现多个位置时不要机械套规则

类型参数可能嵌套在回调、返回对象或其他泛型中,方向会受到组合结构影响。既提供读取又允许任意写入的状态容器,需要满足双方要求,通常比只读生产者更难替换。若使用方法参数,还要额外考虑 TypeScript 的历史兼容行为。

这里讨论的是两个泛型实例之间的关系,例如 Producer<ClickEvent> 与 Producer<EventBase>。函数调用推断出 T 为字符串还是数字,则是在寻找本次参数的具体选择。两者虽然都使用推断一词,但证据和目标不同,不应混成一个规则。

为什么通常不手写方差注解

编译器大多能从结构中得出需要的方差信息。只有遇到明确的复杂循环类型、诊断或经分析确认的性能问题时,才值得考虑注解。注解必须与实际结构关系相容,不能把不安全的消费者强行声明成生产者来绕过报错。

官方文档还指出注解只影响特定实例化比较,不改变任意结构比较。因此它不是实现名义类型或全局强制不变性的工具。维护公共类型时,应优先简化成员关系、使用正确函数语法并验证消费端例子,避免为了显得高级而增加难解释的标记。

回答前,多想一步

容易答错的地方

in/out 可以强制任意对象按指定方向兼容
注解有特定适用范围,不会重写所有结构比较行为;它应符合实际成员关系,不能拿来制造与结构不一致的类型安全承诺。
方差推断就是自动猜出调用参数的具体类型
方差关心替换类型参数后整个容器的兼容方向,调用推断关心某次 T 取什么;理解时要分别列出比较对象与可用证据。
试着用自己的话回答

面试官还会怎么问?

消费者为什么适合用函数属性举例?

严格模式对函数属性参数执行相应兼容检查,而方法声明存在双向兼容例外;用函数属性能更直接展示真实输入能力方向。

同时读写 T 的接口一定可以协变吗?

不能只看返回位置,写入位置也会施加要求;应分析所有成员和嵌套回调,验证较宽引用是否可能写入较窄类型不接受的值。

普通业务代码需要到处标注 in/out 吗?

通常不需要,结构推断已经覆盖多数情况;只有能够说明具体问题、注解含义和验证结果时才应加入,避免增加维护负担。

从一道题,走向一组知识

把知识连起来

泛型设计

TypeScript strictFunctionTypes 为什么检查函数属性,却给方法参数保留双向兼容?

把方法兼容例外与严格函数属性方向分开看

泛型设计

TypeScript 数组为什么允许协变赋值,readonly 怎样减少不安全写入?

可读写容器展示宽视图写入为何可能破坏假设

参考资料

  • TypeScript:Variance Annotations

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 为什么生产与消费方向相反
  3. 同时出现多个位置时不要机械套规则
  4. 为什么通常不手写方差注解
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑