Damnatiox
DOCUMENT / published

泛型、约束、推断、默认参数与变型

泛型、约束、推断、默认参数与变型 泛型用于表达“多个位置共享同一个未知类型”或“输出类型由输入类型决定”。它不是把所有具体类型替换成 <T 。 1. 保留输入输出关系 若改为 unknown unknown ,返回值信息丢失;若改为 any any ,检查也被绕过。泛型的价值是保留关系。 2. 多个类型参数 此例也暴露了标准库动态枚举键会变宽的问题。断言被限制在实现边界,公共签名仍保留 K/V/R 关系。 3. extends 约束 约

TypeScript语言基础 2026/8/265 分钟阅读
# 语言基础# TypeScript# TypeScript语言基础

泛型、约束、推断、默认参数与变型

泛型用于表达“多个位置共享同一个未知类型”或“输出类型由输入类型决定”。它不是把所有具体类型替换成 <T>

1. 保留输入输出关系

TypeScript
function identity<T>(value: T): T { return value } const literal = identity("ready") // "ready"

若改为 unknown -> unknown,返回值信息丢失;若改为 any -> any,检查也被绕过。泛型的价值是保留关系。

2. 多个类型参数

TypeScript
function mapRecord<K extends PropertyKey, V, R>( input: Readonly<Record<K, V>>, transform: (value: V, key: K) => R, ): Record<K, R> { return Object.fromEntries( Object.entries(input).map(([key, value]) => [ key, transform(value as V, key as K), ]), ) as Record<K, R> }

此例也暴露了标准库动态枚举键会变宽的问题。断言被限制在实现边界,公共签名仍保留 K/V/R 关系。

3. extends 约束

TypeScript
function getProperty<T extends object, K extends keyof T>( object: T, key: K, ): T[K] { return object[key] } const user = { id: "u-1", age: 30 } const age = getProperty(user, "age") // number

约束说明类型参数必须具备哪些能力,同时保留具体类型。写成 object: Record<string, unknown> 会丢失精确键和值关联。

4. 默认类型参数与 const 类型参数

TypeScript
type ApiResponse<TData, TMeta = undefined> = { data: TData meta: TMeta } function tuple<const T extends readonly unknown[]>(...values: T): T { return values } const command = tuple("git", "status") // readonly ["git", "status"]

const 类型参数让调用时的字面量推断更精确;它不等于运行时 const,也不冻结参数。

5. 泛型类和接口

TypeScript
interface Repository<TEntity, TId> { findById(id: TId): Promise<TEntity | undefined> save(entity: TEntity): Promise<void> } class MemoryRepository<TEntity extends { id: string }> implements Repository<TEntity, string> { private readonly values = new Map<string, TEntity>() async findById(id: string): Promise<TEntity | undefined> { return this.values.get(id) } async save(entity: TEntity): Promise<void> { this.values.set(entity.id, entity) } }

6. 变型的实用直觉

DogAnimal 的子类型:

  • 只产生 T 的只读容器通常可协变理解:Producer<Dog> 可用于需要 Producer<Animal> 的位置。
  • 只消费 T 的函数参数倾向逆变:能处理所有 Animal 的消费者也能处理 Dog
  • 同时读写 T 的可变容器应谨慎视为不变,否则可能写入错误成员。
TypeScript
interface Producer<out T> { produce(): T } interface Consumer<in T> { consume(value: T): void }

in/out 变型标注用于匹配已存在的结构关系和改善检查性能,不是强制类型变型的开关。日常设计仍应从成员如何使用 T 出发。

7. Java、C++ 泛型类比的边界

  • TypeScript 泛型在 JavaScript 输出中通常被擦除,不能 new T()value instanceof T,除非额外传入构造函数值。
  • Java 泛型也主要采用擦除,但其类/接口关系是名义式,且 JVM 运行模型不同。
  • C++ 模板按实参实例化本机代码,可接受非类型模板参数并参与编译期计算;不能把它等同于 TS 泛型。
TypeScript
type Constructable<T> = new (...args: never[]) => T function create<T>(Constructor: Constructable<T>): T { return new Constructor() }

这里真正用于 new 的是运行时值 Constructor,不是类型参数 T

8. 常见反模式

  1. 只出现一次的类型参数:没有建立关系。
  2. 约束写成 any:失去安全收益。
  3. 过早显式传类型参数:可能压制更精确推断。
  4. 用复杂条件类型解决简单函数重载:降低可读性。
  5. 认为泛型会生成运行时 schema:类型已被擦除。

参考:GenericsType Compatibility