mcpField
L
limit-optional-properties

limit-optional-properties

github.com/marius-townhouse开发645 次安装38 周前更新
代码仓库

关于 limit-optional-properties

这项 TypeScript 技能讲解如何减少接口中的可选属性。它帮助开发者用默认值、独立类型或带标签联合显式表达状态,避免引入不必要的组合。

主要功能

  1. 01用必需属性并提供默认值
  2. 02用独立类型表示不同变体
  3. 03用带标签联合表达选项
  4. 04将相关可选属性分组
  5. 05仅在缺失有意义时使用可选项

适用场景

  1. 01设计带可选字段的接口
  2. 02减少配置类型的状态组合
  3. 03建模成组出现的相关字段
  4. 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