limit-optional-properties について
インターフェースのオプショナルプロパティを減らすための TypeScript スキルです。不要な状態を増やさず、デフォルト値、別々の型、判別可能なユニオンで状態を明示する方法を扱います。
主な機能
- 01必須プロパティとデフォルト値
- 02別々の型でバリアントを表現
- 03判別可能なユニオンで選択肢を表現
- 04関連するオプショナル項目をまとめる
- 05不在に意味がある場合に限定
活用シーン
- 01オプショナルフィールドを持つインターフェースの設計
- 02設定型の状態の組み合わせを削減
- 03一緒に現れる関連フィールドのモデル化
- 04JavaScript から型を移行
インストール
コマンドをコピーしてターミナルで実行します。
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 も設定されている必要がある」と説明するコメントがある
- オプショナルプロパティにアクセスする
?.の連鎖が繰り返し使われている
よくある正当化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「下位互換性があります」 | 代わりに新しいバージョンの型を作成してください |
| 「すべてのユーザーが必要とするわけではありません」 | ユーザーが異なれば、型も異なります |
| 「オプショナルをもう 1 つ追加するだけです」 | 追加するたびに複雑さは倍増します |
クイックリファレンス
// 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;
要点
オプショナルプロパティは、表面化しにくい複雑さを生み出します。
オプショナルプロパティを 1 つ追加するたびに、考えられる状態の数は倍になります。デフォルト値を持つ必須プロパティ、グループ化したオプショナルオブジェクト、または明示的なユニオン型を優先してください。オプショナルプロパティは、不在が意味のある独立した状態である場合にのみ使用してください。
参考資料
Dan Vanderkam 著「Effective TypeScript」の項目 37「Limit the Use of Optional Properties」に基づいています。