limit-optional-properties
npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill limit-optional-properties
Agent 安装分布
Skill 文档
Limit the Use of Optional Properties
Overview
Optional properties are convenient but costly.
Every optional property creates uncertainty. Readers must check if it exists. Code paths multiply. Consider whether required properties or separate types are better.
When to Use This Skill
- Adding new properties to existing types
- Designing interfaces with optional fields
- Migrating types from JavaScript
- Choosing between optional and required
The Iron Rule
Required properties are simpler to work with.
Use optional properties only when absence is meaningful.
Remember:
- Optional = uncertainty in every usage
- Type narrowing is required for optional properties
- Multiple optionals = exponential complexity
- Consider: is absence a valid state?
Detection: Optional Overload
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?
}
}
Combinatorial Explosion
With n optional properties, there are 2^n possible states:
interface Config {
host?: string; // 2 states
port?: number; // x2 = 4 states
timeout?: number; // x2 = 8 states
retries?: number; // x2 = 16 states
}
Many combinations may be invalid!
Alternative 1: Required with Defaults
// 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
};
}
Now Config always has all properties. Simpler to use!
Alternative 2: Separate Types
// 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;
Now the relationship between properties is explicit.
Alternative 3: Tagged Union
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);
}
}
When Optional IS Appropriate
Truly Independent Options
interface RequestOptions {
timeout?: number; // Default behavior is fine
headers?: Headers; // No headers is valid
cache?: boolean; // Default is acceptable
}
These options are independently meaningful and have sensible defaults.
Backward Compatibility
// v1
interface User {
name: string;
}
// v2 - adding optional to avoid breaking changes
interface User {
name: string;
email?: string; // Old code still works
}
But consider: should you version the type instead?
Group Related Optionals
If properties are related, group them:
// 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;
};
}
Now both are present or neither is. See Item 33.
Avoid “God Object” Interfaces
// 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;
}
Pressure Resistance Protocol
1. “Optional is Easier”
Pressure: “Just make it optional, then we don’t have to change existing code”
Response: You’re trading short-term convenience for long-term complexity.
Action: Evaluate: is absence meaningful, or just convenient?
2. “It Might Not Always Be Present”
Pressure: “The data isn’t always available”
Response: Model that explicitly with a union type or separate interface.
Action: Make the state explicit, not implicit via optionality.
Red Flags – STOP and Reconsider
- More than 3-4 optional properties on an interface
- Optional properties that must be checked together
- Comments explaining “if X is set, then Y must be too”
?.chains accessing optional properties repeatedly
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|---|
| “It’s backward compatible” | Create a new type version instead |
| “Not all users need it” | Different users = different types |
| “It’s just one more optional” | Each one doubles complexity |
Quick Reference
// 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;
The Bottom Line
Optional properties create hidden complexity.
Each optional property doubles the number of possible states. Prefer required properties with defaults, grouped optional objects, or explicit union types. Use optional properties only when absence is a meaningful, independent state.
Reference
Based on “Effective TypeScript” by Dan Vanderkam, Item 37: Limit the Use of Optional Properties.