mcpField
E
exclusive-or-properties

exclusive-or-properties

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

关于 exclusive-or-properties

exclusive-or-properties 是一项介绍 TypeScript 互斥属性建模方法的技能,面向需要约束配置、组件属性或 API 参数的开发者。它使用可选 never 属性和联合类型,在编译期限制必须且只能出现一个选项。

主要功能

  1. 01用可选 never 属性约束互斥项
  2. 02通过联合类型定义有效变体
  3. 03覆盖组件属性与 API 参数
  4. 04提供可复用的 XOR 辅助类型
  5. 05强调编译期检查组合

适用场景

  1. 01要求配置对象恰好指定一种来源
  2. 02约束组件只能采用一种操作方式
  3. 03建模互斥的 API 参数形式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill exclusive-or-properties

安装后在 Claude Code 中输入/exclusive-or-properties确认已加载。

详细介绍

使用可选的 Never 属性建模异或

概述

有时,你需要一种类型,其中几个属性必须恰好出现一个,不能多于一个。这种“异或”(XOR)模式常见于组件属性、API 参数和配置对象中。使用类型为 never 的可选属性,可以在编译时强制执行此约束。

与对象联合类型相比,这种技术能提供更好的类型安全性;与运行时检查相比,意图也更加清晰。

何时使用此技巧

  • 几个属性中必须恰好出现一个
  • 对互斥的配置选项进行建模
  • 带有替代配置的组件属性
  • 具有互斥变体的 API 参数
  • 防止属性出现无效组合

铁律

使用可选的 never 属性强制执行“恰好一个”约束。每个变体都将自己的属性设为必需,并将其他属性设为 never。

检测

留意以下无效组合:

// RED FLAGS - Invalid combinations allowed
interface Config {
  url?: string;
  filePath?: string;
  content?: string;
}
// Can pass none, one, or all - too permissive!

// Runtime validation needed:
if ((config.url ? 1 : 0) + (config.filePath ? 1 : 0) + (config.content ? 1 : 0) !== 1) {
  throw new Error('Exactly one source required');
}

问题

interface LoadConfig {
  url?: string;      // Load from URL
  filePath?: string; // Load from file
  content?: string;  // Load from string
}

// All these are allowed, but shouldn't be:
const bad1: LoadConfig = {};  // No source specified
const bad2: LoadConfig = { url: '...', filePath: '...' };  // Two sources
const bad3: LoadConfig = { url: '...', filePath: '...', content: '...' };  // All three

// Runtime check required
function load(config: LoadConfig) {
  const sources = [config.url, config.filePath, config.content].filter(Boolean);
  if (sources.length !== 1) {
    throw new Error('Exactly one source required');
  }
  // ...
}

解决方案:可选的 Never 属性

type LoadConfig =
  | { url: string; filePath?: never; content?: never }
  | { url?: never; filePath: string; content?: never }
  | { url?: never; filePath?: never; content: string };

// Valid - exactly one source:
const good1: LoadConfig = { url: 'https://example.com' };
const good2: LoadConfig = { filePath: '/path/to/file' };
const good3: LoadConfig = { content: 'raw content' };

// Invalid - caught at compile time:
const bad1: LoadConfig = {};  // Error: missing required property
const bad2: LoadConfig = { url: '...', filePath: '...' };  // Error: filePath must be undefined
const bad3: LoadConfig = { url: '...', content: '...' };   // Error: content must be undefined

组件属性示例

type ButtonProps = {
  label: string;
  onClick: () => void;
} & (
  | { href: string; to?: never; onPress?: never }      // External link
  | { href?: never; to: string; onPress?: never }     // Router link
  | { href?: never; to?: never; onPress: () => void } // Custom action
);

// Usage
<Button label="External" href="https://example.com" />
<Button label="Internal" to="/dashboard" />
<Button label="Action" onPress={handlePress} />

// Error: can't combine variants
<Button label="Bad" href="..." to="..." />  // Error!

API 参数示例

type SearchParams = {
  limit?: number;
  offset?: number;
} & (
  | { query: string; filters?: never }
  | { query?: never; filters: Filter[] }
);

// Valid
search({ query: 'typescript' });
search({ filters: [{ field: 'status', value: 'active' }] });
search({ query: 'typescript', limit: 10 });

// Invalid
search({});  // Error: need query or filters
search({ query: '...', filters: [...] });  // Error: can't have both

通用 XOR 辅助类型

type XOR<A, B> =
  | (A & { [K in keyof B]?: never })
  | (B & { [K in keyof A]?: never });

// Usage
type Config = XOR<
  { url: string },
  XOR<
    { filePath: string },
    { content: string }
  >
>;

// Or for exactly one of many:
type ExactlyOne<T> = {
  [K in keyof T]: { [P in K]: T[K] } & {
    [P in Exclude<keyof T, K>]?: never;
  };
}[keyof T];

type LoadConfig2 = ExactlyOne<{
  url: string;
  filePath: string;
  content: string;
}>;

抗压协议

强制执行属性互斥时:

  1. 识别互斥关系:哪些属性互斥?
  2. 创建变体:每个变体都有一个必需属性,其他属性设为 never
  3. 联合变体:使用 | 运算符组合
  4. 测试组合:确保无效组合会报错
  5. 记录此模式:说明某些组合为何无效

危险信号

反模式问题解决方案
所有属性都可选允许一个都不出现或出现多个可选的 Never 模式
仅进行运行时验证错误发生在运行时,而非编译时在类型层面强制执行
注释写着“只能使用一个”没有强制执行让它成为类型错误

常见借口

“我会在运行时验证”

事实:运行时验证会在之后才捕获错误。类型层面的强制执行能立即捕获错误。

“这太冗长了”

事实:使用 XOR 或 ExactlyOne 等辅助类型可以减少重复。

“用户可能想使用多个选项”

事实:如果这样有效,就不要使用 XOR。如果无效,就在类型层面强制执行。

快速参考

模式语法含义
XOR(2 个属性){ a: T, b?: never } | { a?: never, b: T }恰好一个
XOR(3 个或更多属性)变体的联合类型恰好一个
辅助类型XOR<A, B>可复用模式

总结

使用可选的 never 属性,在编译时强制执行“恰好一个”约束。这可以消除一整类运行时错误,并让无效状态无法表示。

参考资料

  • Dan Vanderkam 所著《Effective TypeScript》第 2 版
  • 条目 63:使用可选的 Never 属性建模异或

评论

支持 Markdown