泛型、约束、推断、默认参数与变型
泛型用于表达“多个位置共享同一个未知类型”或“输出类型由输入类型决定”。它不是把所有具体类型替换成 <T>。
1. 保留输入输出关系
function identity<T>(value: T): T {
return value
}
const literal = identity("ready") // "ready"
若改为 unknown -> unknown,返回值信息丢失;若改为 any -> any,检查也被绕过。泛型的价值是保留关系。
2. 多个类型参数
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 约束
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 类型参数
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. 泛型类和接口
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. 变型的实用直觉
设 Dog 是 Animal 的子类型:
- 只产生
T的只读容器通常可协变理解:Producer<Dog>可用于需要Producer<Animal>的位置。 - 只消费
T的函数参数倾向逆变:能处理所有Animal的消费者也能处理Dog。 - 同时读写
T的可变容器应谨慎视为不变,否则可能写入错误成员。
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 泛型。
type Constructable<T> = new (...args: never[]) => T
function create<T>(Constructor: Constructable<T>): T {
return new Constructor()
}
这里真正用于 new 的是运行时值 Constructor,不是类型参数 T。
8. 常见反模式
- 只出现一次的类型参数:没有建立关系。
- 约束写成
any:失去安全收益。 - 过早显式传类型参数:可能压制更精确推断。
- 用复杂条件类型解决简单函数重载:降低可读性。
- 认为泛型会生成运行时 schema:类型已被擦除。