| name | exclusive-or-properties |
| description | Use when exactly one of several properties should be present. Use when modeling mutually exclusive options. Use when building component props with alternative configurations. Use when designing API parameters that have variants. |
Use Optional Never Properties to Model Exclusive Or
Overview
Sometimes you need a type where exactly one of several properties must be present, but not more than one. This "exclusive or" (XOR) pattern is common in component props, API parameters, and configuration objects. Using optional properties with never types enforces this constraint at compile time.
This technique provides better type safety than unions of objects and clearer intent than runtime checks.
When to Use This Skill
- Exactly one of several properties must be present
- Modeling mutually exclusive configuration options
- Component props with alternative configurations
- API parameters that have exclusive variants
- Preventing invalid combinations of properties
The Iron Rule
Use optional never properties to enforce "exactly one of" constraints. Each variant makes its property required and others never.
Detection
Watch for these invalid combinations:
interface Config {
url?: string;
filePath?: string;
content?: string;
}
if ((config.url ? 1 : 0) + (config.filePath ? 1 : 0) + (config.content ? 1 : 0) !== 1) {
throw new Error('Exactly one source required');
}
The Problem
interface LoadConfig {
url?: string;
filePath?: string;
content?: string;
}
const bad1: LoadConfig = {};
const bad2: LoadConfig = { url: '...', filePath: '...' };
const bad3: LoadConfig = { url: '...', filePath: '...', content: '...' };
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');
}
}
The Solution: Optional Never Properties
type LoadConfig =
| { url: string; filePath?: never; content?: never }
| { url?: never; filePath: string; content?: never }
| { url?: never; filePath?: never; content: string };
const good1: LoadConfig = { url: 'https://example.com' };
const good2: LoadConfig = { filePath: '/path/to/file' };
const good3: LoadConfig = { content: 'raw content' };
const bad1: LoadConfig = {};
const bad2: LoadConfig = { url: '...', filePath: '...' };
const bad3: LoadConfig = { url: '...', content: '...' };
Component Props Example
type ButtonProps = {
label: string;
onClick: () => void;
} & (
| { href: string; to?: never; onPress?: never }
| { href?: never; to: string; onPress?: never }
| { href?: never; to?: never; onPress: () => void }
);
<Button label="External" href="https://example.com" />
<Button label="Internal" to="/dashboard" />
<Button label="Action" onPress={handlePress} />
<Button label="Bad" href="..." to="..." />
API Parameters Example
type SearchParams = {
limit?: number;
offset?: number;
} & (
| { query: string; filters?: never }
| { query?: never; filters: Filter[] }
);
search({ query: 'typescript' });
search({ filters: [{ field: 'status', value: 'active' }] });
search({ query: 'typescript', limit: 10 });
search({});
search({ query: '...', filters: [...] });
Generic XOR Helper
type XOR<A, B> =
| (A & { [K in keyof B]?: never })
| (B & { [K in keyof A]?: never });
type Config = XOR<
{ url: string },
XOR<
{ filePath: string },
{ content: string }
>
>;
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;
}>;
Pressure Resistance Protocol
When enforcing exclusive properties:
- Identify exclusivity: Which properties are mutually exclusive?
- Create variants: Each variant has one required, others
never
- Union the variants: Combine with
| operator
- Test combinations: Ensure invalid combos fail
- Document the pattern: Explain why certain combos are invalid
Red Flags
| Anti-Pattern | Problem | Solution |
|---|
| All optional properties | Allows none or many | Optional never pattern |
| Runtime validation only | Errors at runtime, not compile time | Type-level enforcement |
| Comments saying "use only one" | Not enforced | Make it a type error |
Common Rationalizations
"I'll validate at runtime"
Reality: Runtime validation catches bugs later. Type-level enforcement catches them immediately.
"This is too verbose"
Reality: Use helper types like XOR or ExactlyOne to reduce repetition.
"Users might want multiple options"
Reality: If that's valid, don't use XOR. If it's not, enforce it at the type level.
Quick Reference
| Pattern | Syntax | Meaning |
|---|
| XOR (2 props) | { a: T, b?: never } | { a?: never, b: T } | Exactly one |
| XOR (3+ props) | Union of variants | Exactly one |
| Helper type | XOR<A, B> | Reusable pattern |
The Bottom Line
Use optional never properties to enforce "exactly one of" constraints at compile time. This eliminates an entire class of runtime errors and makes invalid states unrepresentable.
Reference
- Effective TypeScript, 2nd Edition by Dan Vanderkam
- Item 63: Use Optional Never Properties to Model Exclusive Or