unify-types について
類似した型を統合するか分けるかを判断する、TypeScript 開発者向けのコーディングスキルです。処理の簡素化と重要な型安全性の維持を比較検討します。
主な機能
- 01類似型を統合するための指針
- 02型を分ける判断基準
- 03タグ付きユニオンの提案
- 04型の絞り込みの重複を確認
活用シーン
- 01わずかに異なる並行インターフェースの見直し
- 02共通プロパティを持つユニオンの簡素化
- 03型の違いが型安全性に重要かの評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill unify-typesインストール後、Claude Code で/unify-typesを入力して読み込まれたか確認します。
詳細
違いをモデル化するより、型を統合する
概要
小さな違いがあるからといって、別々の型にする必要があるとは限りません。
型がほとんど同じ場合、統合するとコードがシンプルになります。小さな違いに対応するコストは、並行する型階層を維持するコストより、たいてい低くなります。
このスキルを使う場面
- 1つか2つのプロパティだけが異なる型
- ほとんど同じ内容のユニオン型
- バリエーションごとに別々の型を作ろうとしているとき
- 複雑な型の関係をシンプルにしたいとき
絶対のルール
Unify types unless differences are fundamental.
Small variations can be handled at runtime.
覚えておきましょう:
- 型が重複すると、処理コードも重複します
- 類似した型のユニオンは、繰り返し型を絞り込むことになります
- 多くの場合、多数の具体的な型より、柔軟な型を1つ使うほうがシンプルです
- 問いかけてみましょう。違いは本当に型システムにとって重要でしょうか?
検出:並行する型
// Two types that are almost identical
interface Dog {
name: string;
breed: string;
barkVolume: number;
}
interface Cat {
name: string;
breed: string;
meowVolume: number;
}
type Pet = Dog | Cat;
// Every function needs to narrow:
function getPetInfo(pet: Pet): string {
if ('barkVolume' in pet) {
return `${pet.name} barks at ${pet.barkVolume}`;
} else {
return `${pet.name} meows at ${pet.meowVolume}`;
}
}
統合する方法
interface Pet {
name: string;
breed: string;
vocalizationType: 'bark' | 'meow';
vocalizationVolume: number;
}
function getPetInfo(pet: Pet): string {
return `${pet.name} ${pet.vocalizationType}s at ${pet.vocalizationVolume}`;
}
型の絞り込みは不要です。1つの型で両方のケースに対応できます。
型を分けたままにする場合
根本的に異なる動作
// These really are different
interface File {
path: string;
read(): Buffer;
write(data: Buffer): void;
}
interface Directory {
path: string;
list(): string[];
create(name: string): void;
}
ファイルとディレクトリでは操作が異なります。統合すると型安全性が失われます。
要素数の違い
// Different structures
interface SingleResult {
value: number;
}
interface MultipleResults {
values: number[];
average: number;
}
これらは実際に異なる形をしています。
実践例:API レスポンス
// Separate types
interface SuccessResponse {
status: 'success';
data: Data;
timestamp: Date;
}
interface ErrorResponse {
status: 'error';
error: string;
timestamp: Date;
}
type Response = SuccessResponse | ErrorResponse;
// Handling requires narrowing everywhere
function logResponse(res: Response) {
console.log(`${res.timestamp}: ${res.status}`);
if (res.status === 'success') {
console.log(res.data); // Need to narrow
}
}
統合したほうがシンプルかどうかを検討します。
// Unified type
interface Response {
status: 'success' | 'error';
data?: Data; // Present on success
error?: string; // Present on error
timestamp: Date;
}
// Can access common fields without narrowing
function logResponse(res: Response) {
console.log(`${res.timestamp}: ${res.status}`);
if (res.data) {
console.log(res.data);
}
}
中間案としてのタグ付きユニオン
タグ付きユニオンがちょうどよいバランスになる場合もあります。
// Tagged union: explicit about differences, unified handling
interface Animal {
name: string;
breed: string;
}
interface Dog extends Animal {
type: 'dog';
barkVolume: number;
}
interface Cat extends Animal {
type: 'cat';
meowVolume: number;
}
type Pet = Dog | Cat;
// Common operations don't need narrowing
function getName(pet: Pet): string {
return pet.name; // Works for both
}
// Type-specific operations are explicit
function getVolume(pet: Pet): number {
return pet.type === 'dog' ? pet.barkVolume : pet.meowVolume;
}
費用対効果の分析
別々の型を作る前に、次の点を確認してください。
-
型固有の動作が必要になる頻度はどのくらいですか?
- まれ → 統合する
- 頻繁 → 分ける
-
違いは構造上のものですか、それとも意味上のものですか?
- 構造は同じで、意味が異なる → 統合を検討する
- 構造が異なる → 分けたままにする
-
統合すると重要な型安全性が失われますか?
- はい → 分けたままにする
- いいえ → 統合する
実例:イベント
// Over-differentiated
interface ClickEvent {
type: 'click';
x: number;
y: number;
target: Element;
}
interface KeyEvent {
type: 'key';
key: string;
target: Element;
}
interface ScrollEvent {
type: 'scroll';
scrollTop: number;
target: Element;
}
共通部分を統合できます。
interface BaseEvent {
type: string;
target: Element;
}
interface ClickEvent extends BaseEvent {
type: 'click';
x: number;
y: number;
}
// etc.
違いがほとんど問題にならない場合は、すべてを統合することもできます。
interface UIEvent {
type: 'click' | 'key' | 'scroll';
target: Element;
details: ClickDetails | KeyDetails | ScrollDetails;
}
圧力に対する対応手順
1. 「型は正確であるべき」
圧力:「型を分けたほうが正確です」
**回答:**正確さには、コードの複雑さという代償があります。
**行動:**正確さの利点と、処理の複雑さを比較します。
2. 「将来、分岐するかもしれない」
圧力:「将来の柔軟性のために、分けておきましょう」
**回答:**それは YAGNI です。実際に分岐したらリファクタリングしましょう。
**行動:**まず統合し、必要になったら後で分けます。
危険信号 — いったん立ち止まって再検討する
- プロパティの大半が共通するユニオン型
- 類似した型に対する型の絞り込みが繰り返されている
- プロパティ名が1つ違うだけの型
- 「異なる」型に対して、並行する実装がある
よくある正当化(すべて誤り)
| 言い訳 | 実態 |
|---|---|
| 「概念的に違う」 | コードは概念を気にしません |
| 「型を分けたほうがすっきりする」 | 型が増えるほど、処理コードも増えます |
| 「区別が必要になるかもしれない」 | 必要になったときに考えればよいことです |
クイックリファレンス
// DON'T: Separate types for minor differences
interface Dog { name: string; bark: () => void; }
interface Cat { name: string; meow: () => void; }
type Pet = Dog | Cat;
// DO: Unified type
interface Pet {
name: string;
sound: 'bark' | 'meow';
makeSound: () => void;
}
// DO: Keep separate when genuinely different
interface File { read(): Buffer; write(b: Buffer): void; }
interface Directory { list(): string[]; }
要点
違いが根本的なものでない限り、型を統合しましょう。
型を分けると、あらゆる場所で別々の処理コードが必要になります。ほとんどのプロパティが共通し、細部だけが異なる場合は、オプションフィールドやバリエーションフィールドを持つ統合された型のほうが、たいていシンプルです。
参考文献
Dan Vanderkam 著「Effective TypeScript」、項目 39:「違いをモデル化するより、型を統合する」より。