关于 generics-as-functions
这项 TypeScript 技能将泛型理解为作用于类型的函数,引导开发者设置约束、选择名称并编写文档,帮助设计可复用的泛型类型与函数。
主要功能
- 01将泛型视为类型转换
- 02使用 extends 约束参数
- 03选择描述性类型参数名称
- 04用 @template 记录参数
- 05识别有问题的泛型模式
适用场景
- 01定义可复用的类型工具
- 02设计泛型函数和类
- 03阻止无效的泛型实参
- 04完善公开泛型的文档
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill generics-as-functions安装后在 Claude Code 中输入/generics-as-functions确认已加载。
详细介绍
将泛型视为类型之间的函数
概述
泛型类型相当于值空间中函数在类型层面的对应形式。正如函数接收参数并返回值,泛型类型接收类型参数并生成具体类型。这个思维模型能帮助你编写更好的泛型类型:运用编写函数时的相同原则,包括约束输入、选择恰当的名称和记录行为。
将泛型理解为类型之间的函数,有助于明确何时使用约束、如何命名类型参数,以及为什么某些泛型模式有效而另一些无效。这种视角对于在 TypeScript 中有效进行类型层面编程至关重要。
何时使用此技能
- 定义可转换其他类型的泛型类型
- 编写带有类型参数的泛型函数
- 限制可传入泛型的类型
- 使用 TSDoc 记录泛型类型
- 创建可复用的类型工具
核心规则
将泛型类型视为类型之间的函数:使用 extends 像类型注解一样约束输入,选择描述性名称,并使用 @template TSDoc 记录文档。
检测
留意以下模式:
// RED FLAGS - Poor generic design
type MyPick<T, K> = { [P in K]: T[P] }; // No constraints, errors in implementation
type BadGeneric<X, Y, Z> = ...; // Single-letter names without context
function parse<T>(input: string): T; // Return-only generic, no better than any
将泛型类型视为函数
泛型类型接收类型参数并生成具体类型:
// Generic type "function"
type MyPartial<T> = { [K in keyof T]?: T[K] };
// "Calling" the function with Person
type PartPerson = MyPartial<Person>;
// Equivalent to: { name?: string; age?: number; }
与函数一样,泛型类型可以有多个参数:
// Two type parameters
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Usage
type NameOnly = MyPick<Person, 'name'>;
// Equivalent to: { name: string }
约束类型参数
使用 extends 约束类型参数,就像使用类型注解约束函数参数一样:
// GOOD: Constrained type parameters
type MyPick<T extends object, K extends keyof T> = {
[P in K]: T[P]
};
// Without constraints - allows invalid instantiations
type BadPick<T, K> = { [P in K]: T[P] }; // Errors in implementation
// Invalid uses caught by constraints:
type Bad1 = MyPick<Person, 'firstName'>; // Error: 'firstName' not in Person
type Bad2 = MyPick<'age', Person>; // Error: string doesn't satisfy object
命名类型参数
选择描述性名称,尤其是对于复杂的泛型:
// Short names OK for simple, local generics
type Partial<T> = { [K in keyof T]?: T[K] };
// Longer names for complex or exported generics
type MapValues<
ObjectType extends object,
ValueTransformer extends (value: any) => any
> = {
[Key in keyof ObjectType]: ValueTransformer<ObjectType[Key]>
};
记录泛型
使用 @template TSDoc 标签记录类型参数:
/**
* Construct a new object type using a subset of properties from another.
* @template T - The original object type
* @template K - The keys to pick, typically a union of string literal types
*/
type MyPick<T extends object, K extends keyof T> = {
[P in K]: T[P]
};
泛型函数
泛型函数定义相关的泛型类型,并支持类型推断:
function pick<T extends object, K extends keyof T>(
obj: T,
...keys: K[]
): Pick<T, K> {
const result: Partial<Pick<T, K>> = {};
for (const k of keys) {
result[k] = obj[k];
}
return result as Pick<T, K>;
}
// TypeScript infers types from arguments
const person = { name: 'Alice', age: 30 };
const nameOnly = pick(person, 'name');
// Type: Pick<{ name: string; age: number }, 'name'>
泛型类
泛型类用于保存无需传递给方法的类型:
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
// Type inferred from constructor
const dateBox = new Box(new Date());
// Type: Box<Date>
抗压规则
当有人施压要求使用无限制的泛型时:
- 添加约束:使用
extends限制有效的类型实参 - 考虑默认值:为类型参数提供合理的默认值
- 记录要求:使用 TSDoc 解释约束
- 测试边缘情况:验证泛型适用于联合类型和边缘情况
危险信号
| 反模式 | 问题所在 |
|---|---|
| 不受约束的类型参数 | 允许无效实例化,导致实现错误 |
| 复杂泛型中使用单字母名称 | 降低可读性 |
| 仅用于返回值的泛型 | 等同于类型断言,不提供类型安全性 |
| 公共泛型缺少 TSDoc | 开发者体验不佳 |
常见借口
“约束会限制灵活性”
实际情况:约束能在类型层面捕获错误,避免产生令人困惑的类型错误或错误类型。它们还会说明有效用法。
“T、K、V 是标准名称”
实际情况:简单情况下使用这些惯例名称没问题,但在复杂泛型中,描述性名称更易读。名称长度应与作用域相匹配。
“用户能自行弄清楚类型”
实际情况:文档能帮助用户理解泛型,而无需阅读实现。IDE 工具提示中会显示 @template 标签。
快速参考
| 概念 | 值层面 | 类型层面 |
|---|---|---|
| 定义 | function | type |
| 参数 | (x: T) | <T extends Constraint> |
| 返回 | : ReturnType | = ResultType |
| 文档 | @param | @template |
| 约束 | 类型注解 | extends 关键字 |
总结
泛型类型是类型之间的函数。运用编写函数时的相同原则:约束输入、选择有意义的名称,并充分记录文档。这个思维模型能让复杂的类型层面代码更易理解和维护。
参考资料
- Dan Vanderkam 著《Effective TypeScript》第 2 版
- 条目 50:将泛型视为类型之间的函数