先记住这个答案
泛型函数可以用同一个类型参数连接输入和返回值。例如 identity<T>(value: T): T 在一次调用中捕获输入类型,并让返回结果保留这个类型;传入带有额外字段的对象,返回结果仍可使用这些字段。any 会丢掉相关检查,unknown 则要求调用者重新证明结果类型,普通联合返回也不一定保留本次输入是哪一支。泛型描述的是类型关系,不能单靠签名证明返回值与输入是同一个对象,也不会验证外部数据真的满足调用者指定的类型。
- 相同类型参数连接同一次调用里的多个位置
- any 与 unknown 不自动保存输入输出的对应关系
- 泛型契约不会在运行时校验数据或保证引用相同
为什么直接写 any 会丢失关系
返回 any 的通用函数允许调用者随意使用不存在的成员,输入是数字也可能在返回后调用字符串方法。unknown 虽然保持了检查责任,却没有告诉调用者结果与输入相同。泛型可以在保持通用性的同时,避免每个调用点重复断言。
下面函数分别原样返回输入和把输入放进对象。它们不需要知道用户对象的全部字段,只需要保证返回结构中的相应位置仍然对应同一个 T。数字调用的结果不能使用字符串方法,说明通用并没有变成跳过类型检查。
function identity<T>(value: T): T { return value; }
function wrap<T>(value: T): { value: T } { return { value }; }
const user = identity({ id: 7, name: 'Lin' });
const boxed = wrap({ enabled: true, retries: 2 });
const count = identity(12);
// @ts-expect-error 数字结果没有 trim 方法
count.trim();user 保留标识和名称,boxed.value 保留配置字段,数字结果则仍受数字类型约束。示例中的 identity 实现确实返回原值,但这种引用行为来自函数体,不能仅从泛型签名推导。
为什么联合返回不总能表达对应关系
若函数输入和返回都直接声明为 string | number,传入字符串后得到的静态返回类型仍可能是这个联合,因为签名没有保证本次输出跟随输入分支。泛型用同一个参数建立关联,重载也可以描述有限输入与输出对应关系,应按接口规模选择。
在函数实现内部,T 可能代表任何允许的类型,所以不能直接读取 length 或调用专有方法。需要操作某种能力时,可以增加相应约束;如果函数根本不需要该能力,就保持实现只做对所有输入都成立的事情,避免过度限制调用者。
保持类型不等于保证数据正确
声明一个没有输入来源的 parse<T> 并把任意 JSON 断言为 T,并没有通过泛型获得验证能力。这里的类型只是调用者自行选择的承诺,解析结果可能完全不符合它。真正的边界函数应接收解析器或执行结构检查后再返回具体类型。
同样,泛型函数可以在满足签名的前提下返回另一个同类型值,不能把 T 到 T 理解为业务值必定不变。需要保证引用身份、排序稳定或不修改输入时,应把这些运行行为写进契约并验证,而不是指望类型参数替代行为测试。
容易答错的地方
- 泛型就是给 any 换一个好看的名字
- 类型参数能在多个位置保持约束关系,调用方仍受到成员检查;any 往往让不相关操作直接通过,两者对后续可靠性的影响不同。
- 写了返回 T 就能自动解析出 T 的真实数据
- 类型参数在运行时不可用,无法凭它检查外部字段;需要实际解析逻辑或受控数据来源,不能只用断言宣称返回值可信。
面试官还会怎么问?
什么时候用 unknown 比泛型更合适?
当函数只接收未知输入、并不承诺输出保留输入类型关系时,unknown 可以清楚表达检查边界;例如解析器先验证再返回固定业务结构。
返回值使用 T 就一定保留字面量吗?
不一定,具体推断还受输入形式、可变性、约束和上下文影响;需要字面量级别信息时,应在实际编译器中核对推断结果。
每次调用必须手写类型参数吗?
通常可以由输入和上下文推断,显式参数适合证据不足或需要更明确契约的场景;它仍然要满足约束,不能把不合法输入变成合法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。