关于 exclusive-or-properties
exclusive-or-properties 是一项介绍 TypeScript 互斥属性建模方法的技能,面向需要约束配置、组件属性或 API 参数的开发者。它使用可选 never 属性和联合类型,在编译期限制必须且只能出现一个选项。
主要功能
- 01用可选 never 属性约束互斥项
- 02通过联合类型定义有效变体
- 03覆盖组件属性与 API 参数
- 04提供可复用的 XOR 辅助类型
- 05强调编译期检查组合
适用场景
- 01要求配置对象恰好指定一种来源
- 02约束组件只能采用一种操作方式
- 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;
}>;
抗压协议
强制执行属性互斥时:
- 识别互斥关系:哪些属性互斥?
- 创建变体:每个变体都有一个必需属性,其他属性设为
never - 联合变体:使用
|运算符组合 - 测试组合:确保无效组合会报错
- 记录此模式:说明某些组合为何无效
危险信号
| 反模式 | 问题 | 解决方案 |
|---|---|---|
| 所有属性都可选 | 允许一个都不出现或出现多个 | 可选的 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