关于 limit-optional-properties
这项 TypeScript 技能讲解如何减少接口中的可选属性。它帮助开发者用默认值、独立类型或带标签联合显式表达状态,避免引入不必要的组合。
主要功能
- 01用必需属性并提供默认值
- 02用独立类型表示不同变体
- 03用带标签联合表达选项
- 04将相关可选属性分组
- 05仅在缺失有意义时使用可选项
适用场景
- 01设计带可选字段的接口
- 02减少配置类型的状态组合
- 03建模成组出现的相关字段
- 04从 JavaScript 迁移类型
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill limit-optional-properties安装后在 Claude Code 中输入/limit-optional-properties确认已加载。
详细介绍
限制可选属性的使用
概述
可选属性虽然方便,但代价不小。
每个可选属性都会带来不确定性。读者必须检查它是否存在,代码路径也会增加。请考虑必需属性或单独的类型是否更合适。
何时使用此技能
- 向现有类型添加新属性
- 设计包含可选字段的接口
- 从 JavaScript 迁移类型
- 在可选属性和必需属性之间做选择
铁律
Required properties are simpler to work with.
Use optional properties only when absence is meaningful.
请记住:
- 可选 = 每次使用时都存在不确定性
- 可选属性需要进行类型收窄
- 多个可选属性 = 复杂度呈指数级增长
- 请考虑:缺失是否属于有效状态?
检测:可选属性过多
interface FormattedValue {
value: number;
units: string;
unitSystem?: 'metric' | 'imperial'; // New optional property
}
function formatValue(val: FormattedValue): string {
// Now EVERY usage must consider: is unitSystem set?
if (val.unitSystem === 'metric') {
// ...
} else if (val.unitSystem === 'imperial') {
// ...
} else {
// undefined case - what does it mean?
}
}
组合爆炸
有 n 个可选属性时,就有 2^n 种可能状态:
interface Config {
host?: string; // 2 states
port?: number; // x2 = 4 states
timeout?: number; // x2 = 8 states
retries?: number; // x2 = 16 states
}
其中许多组合可能无效!
替代方案 1:使用默认值的必需属性
// Instead of optional properties
interface Config {
host: string;
port: number;
timeout: number;
}
// Provide defaults at construction
function createConfig(overrides: Partial<Config>): Config {
return {
host: 'localhost',
port: 8080,
timeout: 5000,
...overrides
};
}
现在 Config 始终包含所有属性,使用起来更简单!
替代方案 2:单独的类型
// Instead of one type with optionals
interface BasicFormattedValue {
value: number;
units: string;
}
interface LocalizedFormattedValue {
value: number;
units: string;
unitSystem: 'metric' | 'imperial';
}
type FormattedValue = BasicFormattedValue | LocalizedFormattedValue;
现在属性之间的关系一目了然。
替代方案 3:带标签的联合类型
type FormattedValue =
| { type: 'basic'; value: number; units: string }
| { type: 'localized'; value: number; units: string; unitSystem: 'metric' | 'imperial' };
function format(val: FormattedValue): string {
switch (val.type) {
case 'basic':
return `${val.value} ${val.units}`;
case 'localized':
// val.unitSystem is guaranteed to exist here
return formatLocalized(val);
}
}
何时适合使用可选属性
真正独立的选项
interface RequestOptions {
timeout?: number; // Default behavior is fine
headers?: Headers; // No headers is valid
cache?: boolean; // Default is acceptable
}
这些选项各自都有意义,并且有合理的默认值。
向后兼容
// v1
interface User {
name: string;
}
// v2 - adding optional to avoid breaking changes
interface User {
name: string;
email?: string; // Old code still works
}
不过请考虑:是否应该为该类型创建新版本?
将相关的可选属性分组
如果属性彼此相关,请将它们分组:
// Bad: related properties are separately optional
interface Person {
name: string;
birthPlace?: string; // If one is set...
birthDate?: Date; // ...the other probably should be too
}
// Good: grouped together
interface Person {
name: string;
birth?: {
place: string;
date: Date;
};
}
现在这两个属性要么同时存在,要么都不存在。请参阅条目 33。
避免“上帝对象”接口
// Bad: too many optionals
interface UserProfile {
id: string;
name?: string;
email?: string;
avatar?: string;
preferences?: Preferences;
settings?: Settings;
lastLogin?: Date;
// ... 20 more optional fields
}
// Better: compose specific types
interface User {
id: string;
name: string;
}
interface UserWithProfile extends User {
email: string;
avatar: string;
}
interface UserWithPreferences extends User {
preferences: Preferences;
}
抵御压力的方案
1. “可选属性更简单”
压力:“直接设为可选就行,这样我们不用修改现有代码。”
**回应:**你是在用长期复杂度换取短期便利。
**行动:**评估缺失是否有意义,还是仅仅图方便?
2. “它不一定总是存在”
压力:“数据并非总是可用。”
**回应:**使用联合类型或单独的接口来明确建模这种情况。
**行动:**明确表达该状态,而不是通过可选性隐式表达。
危险信号——停止并重新考虑
- 一个接口上有超过 3-4 个可选属性
- 必须一起检查的可选属性
- 注释中解释“如果设置了 X,则 Y 也必须设置”
- 反复使用
?.链访问可选属性
常见的合理化说辞(均不成立)
| 借口 | 事实 |
|---|---|
| “这样向后兼容” | 应改为创建新版本的类型 |
| “并非所有用户都需要它” | 不同用户 = 不同类型 |
| “只多一个可选属性而已” | 每增加一个,复杂度就翻倍 |
快速参考
// DON'T: Many independent optionals
interface Bad {
a?: number;
b?: string;
c?: boolean;
d?: Date;
}
// DO: Required with defaults
interface Good {
a: number;
b: string;
}
const good: Good = { a: 1, b: '', ...overrides };
// DO: Group related properties
interface Person {
name: string;
contact?: { email: string; phone: string };
}
// DO: Use union for different shapes
type Value = SimpleValue | DetailedValue;
归根结底
可选属性会带来隐藏的复杂度。
每个可选属性都会使可能状态的数量翻倍。优先使用带默认值的必需属性、分组的可选对象或显式联合类型。只有在缺失本身具有独立意义时,才使用可选属性。
参考资料
改编自 Dan Vanderkam 的《Effective TypeScript》,条目 37:限制可选属性的使用。
评论
支持 Markdown