使用类型操作符和泛型避免重复
核心原则:应用 DRY 原则于类型定义
在软件开发中,遵循 DRY (Don't Repeat Yourself) 原则至关重要。类型定义中的重复(Type Duplication)会带来维护困难,增加代码在未来发生分歧(diverge)的风险。通过学习 TypeScript 提供的类型操作(Type Operations)和泛型(Generic Types),可以将 DRY 原则应用到类型定义中,从而实现类型定义的抽象化和同步化。
减少类型重复的基础方法
命名类型
避免重复的最简单方法是为类型结构赋予一个名称,并在需要的地方复用它。这相当于在类型系统中提取一个“常量”:
// 不推荐:重复的内联类型定义
function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
// 推荐:提取并命名类型
interface Point2D {
x: number;
y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ } //如果多个函数共享同一函数签名,应提取一个命名类型(遵循 Item 12 的建议)。
使用 extends 或交叉类型
当一个类型是另一个类型的超集时,可以使用 extends 关键字来消除重复,只写出新增的字段。如果多个类型共享一个字段子集,可以抽象出一个基类型(Base Interface)。
interface Person {
firstName: string;
lastName: string;
}
// 扩展接口
interface PersonWithBirthDate extends Person {
birth: Date; // 只定义新增字段,避免重复
}
// 提取基础接口
interface Vertebrate {
weightGrams: number;
color: string;
}
interface Bird extends Vertebrate {
wingspanCm: number;
} //您也可以使用交叉类型操作符 & 来扩展现有类型,这在向联合类型添加属性时特别有用:
type PersonWithBirthDateAlt = Person & { birth: Date }; //使用类型操作进行类型映射
当需要从现有类型派生出新类型时,可以使用类型操作工具。
索引访问类型(Indexing into Types)
可以使用索引访问类型(T['K'])来获取另一个类型中特定属性的类型,确保类型定义与源头保持同步。
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: State['userId']; // 从 State 中继承类型
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
} //索引访问类型也常用于从标签联合类型(Tagged Unions)中提取所有标签的联合类型,从而避免手动重复书写标签名称:
interface SaveAction { type: 'save'; }
interface LoadAction { type: 'load'; }
type Action = SaveAction | LoadAction;
// 提取所有 type 属性的联合类型:"save" | "load"
type ActionType = Action['type']; //泛型工具类型和映射类型
泛型(Generic Types) 是类型层面的“函数”,它们接受类型参数并生成具体类型。映射类型(Mapped Types)则是一种在类型系统中的“循环”,允许遍历一个类型的键,并根据键和值创建新类型。keyof 操作符用于获取类型的所有键的联合类型。
内置的工具类型(Utility Types)利用这些机制帮助我们避免重复:
| 工具类型 | 目的 | 示例 |
| :--- | :--- | :--- |
| `Pick<T, K>` | 从类型 `T` 中选取属性 `K` 的子集来构建新类型。 | `type UserView = Pick<State, 'userId' | 'pageTitle'>;` |
| `Partial<T>` | 将类型 `T` 的所有属性标记为可选(Optional)。 | `type OptionsUpdate = Partial<Options>;` |
| `ReturnType<T>` | 获取函数类型 `T` 的返回值类型。 | `type UserInfo = ReturnType<typeof getUserInfo>;` |映射类型的基本结构如下(例如实现 Partial):
// 遍历 Options 的所有键,并为每个键添加可选修饰符 (?)
type OptionsUpdate = {[k in keyof Options]?: Options[k]}; //
// 映射类型可以保留修饰符 (Homomorphic Mapped Types)
interface Customer {
/** 客户姓名 */
readonly name: string;
}
// Pick 是同构映射,会保留 readonly 修饰符和 TSDoc 文档
type PickName = Pick<Customer, 'name'>;键重命名(Key Renaming)
映射类型还支持使用 as 子句来重命名属性键。这在处理键值转换或规范化时非常有用:
interface ShortToLong {
q: 'search';
n: 'numberOfResults';
}
// 将 ShortToLong 的值作为新类型的键,键作为新类型的值
type LongToShort = { [k in keyof ShortToLong as ShortToLong[k]]: k };
// LongToShort 的类型结果为 { search: "q"; numberOfResults: "n"; }从运行时值推导类型
如果运行时值是某一类型的唯一“事实来源”(source of truth),可以使用 typeof 操作符从该值推导出其静态类型。
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
// Options 的类型将精确匹配 INIT_OPTIONS 的结构 (例如 width: number)
type Options = typeof INIT_OPTIONS; 避免过度抽象(Premature Abstraction)
虽然 DRY 原则很重要,但应避免不恰当的抽象。如果两个类型(例如 Product 和 Customer)恰好共享一些结构相同的属性(如 id: number, name: string),但它们在语义上代表不同的概念,并且可能独立演变,则不应强制抽象出共同的基类型。
引用格言:“重复远比错误的抽象便宜得多”(duplication is far cheaper than the wrong abstraction)。
总结要点
- DRY 原则同样适用于类型定义。应使用类型操作和泛型来避免在类型结构中重复编写代码。
- 通过命名类型和使用
extends或&交叉类型来减少重复定义。 - 泛型是类型层面的“函数”,用于抽象和重用类型逻辑。
- 熟悉并使用内置的工具类型,如
Pick、Partial和ReturnType,来代替手动编写映射逻辑。 - 掌握
keyof、typeof、索引访问和映射类型等工具,以实现类型之间的灵活转换。 - 避免过度抽象:确保您共享的属性在语义上是相同的,而不是仅仅在结构上巧合一致。
上次更新于