关于 unify-types
面向 TypeScript 开发者的编码技能,帮助判断相似类型应合并还是分开。它权衡简化处理与保留重要类型安全的需求。
主要功能
- 01合并相似类型的指导
- 02保留独立类型的判断标准
- 03标记联合类型建议
- 04检查重复的类型收窄
适用场景
- 01检查差异较小的并行接口
- 02简化属性相似的联合类型
- 03评估类型差异对类型安全是否重要
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill unify-types安装后在 Claude Code 中输入/unify-types确认已加载。
详细介绍
优先统一类型,而不是对差异建模
概述
细微差异通常不值得单独定义类型。
当类型几乎相同时,统一它们可以简化代码。处理细微差异的成本通常低于维护并行的类型层次结构。
何时使用此方法
- 仅有一两个属性不同的类型
- 大部分相同的联合类型
- 正在考虑为不同变体定义单独的类型
- 简化复杂的类型关系
铁律
Unify types unless differences are fundamental.
Small variations can be handled at runtime.
请记住:
- 重复的类型 = 重复的处理代码
- 相似类型的联合 = 反复进行类型收窄
- 一个灵活的类型通常比多个特定类型更简单
- 不妨问问:这些差异真的对类型系统重要吗?
检测:并行类型
// Two types that are almost identical
interface Dog {
name: string;
breed: string;
barkVolume: number;
}
interface Cat {
name: string;
breed: string;
meowVolume: number;
}
type Pet = Dog | Cat;
// Every function needs to narrow:
function getPetInfo(pet: Pet): string {
if ('barkVolume' in pet) {
return `${pet.name} barks at ${pet.barkVolume}`;
} else {
return `${pet.name} meows at ${pet.meowVolume}`;
}
}
统一方案
interface Pet {
name: string;
breed: string;
vocalizationType: 'bark' | 'meow';
vocalizationVolume: number;
}
function getPetInfo(pet: Pet): string {
return `${pet.name} ${pet.vocalizationType}s at ${pet.vocalizationVolume}`;
}
无需进行类型收窄。一个类型就能处理两种情况。
何时保留单独的类型
行为存在根本差异
// These really are different
interface File {
path: string;
read(): Buffer;
write(data: Buffer): void;
}
interface Directory {
path: string;
list(): string[];
create(name: string): void;
}
文件和目录的操作不同。将它们统一会失去类型安全性。
基数不同
// Different structures
interface SingleResult {
value: number;
}
interface MultipleResults {
values: number[];
average: number;
}
它们的形状确实不同。
实用示例:API 响应
// Separate types
interface SuccessResponse {
status: 'success';
data: Data;
timestamp: Date;
}
interface ErrorResponse {
status: 'error';
error: string;
timestamp: Date;
}
type Response = SuccessResponse | ErrorResponse;
// Handling requires narrowing everywhere
function logResponse(res: Response) {
console.log(`${res.timestamp}: ${res.status}`);
if (res.status === 'success') {
console.log(res.data); // Need to narrow
}
}
考虑一下统一后是否更简单:
// Unified type
interface Response {
status: 'success' | 'error';
data?: Data; // Present on success
error?: string; // Present on error
timestamp: Date;
}
// Can access common fields without narrowing
function logResponse(res: Response) {
console.log(`${res.timestamp}: ${res.status}`);
if (res.data) {
console.log(res.data);
}
}
折中方案:带标签的联合类型
有时,带标签的联合类型是恰当的折中方案:
// Tagged union: explicit about differences, unified handling
interface Animal {
name: string;
breed: string;
}
interface Dog extends Animal {
type: 'dog';
barkVolume: number;
}
interface Cat extends Animal {
type: 'cat';
meowVolume: number;
}
type Pet = Dog | Cat;
// Common operations don't need narrowing
function getName(pet: Pet): string {
return pet.name; // Works for both
}
// Type-specific operations are explicit
function getVolume(pet: Pet): number {
return pet.type === 'dog' ? pet.barkVolume : pet.meowVolume;
}
成本效益分析
创建单独的类型之前,先问问自己:
-
我有多常需要特定类型的行为?
- 很少 → 统一
- 经常 → 分开
-
差异是结构性的还是语义上的?
- 结构相同、含义不同 → 可以考虑统一
- 结构不同 → 保持分开
-
统一会失去重要的类型安全性吗?
- 会 → 保持分开
- 不会 → 统一
实例:事件
// Over-differentiated
interface ClickEvent {
type: 'click';
x: number;
y: number;
target: Element;
}
interface KeyEvent {
type: 'key';
key: string;
target: Element;
}
interface ScrollEvent {
type: 'scroll';
scrollTop: number;
target: Element;
}
可以统一共有部分:
interface BaseEvent {
type: string;
target: Element;
}
interface ClickEvent extends BaseEvent {
type: 'click';
x: number;
y: number;
}
// etc.
如果差异很少影响实际使用,也可以完全统一:
interface UIEvent {
type: 'click' | 'key' | 'scroll';
target: Element;
details: ClickDetails | KeyDetails | ScrollDetails;
}
抵制压力的原则
1. “类型应该精确”
压力:“单独的类型更准确”
**回应:**精确性会增加代码复杂度。
**行动:**权衡精确性的好处与处理复杂度。
2. “它们以后可能会分化”
压力:“先分开,以便将来灵活调整”
**回应:**这就是 YAGNI。如果它们真的分化,再重构。
**行动:**现在先统一;有需要时再分开。
危险信号——停下来重新考虑
- 大多数属性都相同的联合类型
- 为相似类型反复编写类型收窄代码
- 仅有一个属性名称不同的类型
- 为“不同”类型编写并行实现
常见借口(均不成立)
| 借口 | 事实 |
|---|---|
| “它们在概念上不同” | 代码不关心概念 |
| “单独的类型更简洁” | 类型越多,处理代码就越多 |
| “我们可能需要区分它们” | 真遇到问题时再处理 |
快速参考
// DON'T: Separate types for minor differences
interface Dog { name: string; bark: () => void; }
interface Cat { name: string; meow: () => void; }
type Pet = Dog | Cat;
// DO: Unified type
interface Pet {
name: string;
sound: 'bark' | 'meow';
makeSound: () => void;
}
// DO: Keep separate when genuinely different
interface File { read(): Buffer; write(b: Buffer): void; }
interface Directory { list(): string[]; }
总结
除非差异是根本性的,否则就统一类型。
单独的类型意味着到处都要编写单独的处理代码。当类型的大部分属性相同、只有细节不同,用带可选字段或变体字段的统一类型通常更简单。
参考资料
基于 Dan Vanderkam 的《Effective TypeScript》第 39 条:优先统一类型,而不是对差异建模。
评论
支持 Markdown