mcpField
U
unify-types

unify-types

github.com/marius-townhouse開発645 回インストール38 週間前に更新
リポジトリ

unify-types について

類似した型を統合するか分けるかを判断する、TypeScript 開発者向けのコーディングスキルです。処理の簡素化と重要な型安全性の維持を比較検討します。

主な機能

  1. 01類似型を統合するための指針
  2. 02型を分ける判断基準
  3. 03タグ付きユニオンの提案
  4. 04型の絞り込みの重複を確認

活用シーン

  1. 01わずかに異なる並行インターフェースの見直し
  2. 02共通プロパティを持つユニオンの簡素化
  3. 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;
}

費用対効果の分析

別々の型を作る前に、次の点を確認してください。

  1. 型固有の動作が必要になる頻度はどのくらいですか?

    • まれ → 統合する
    • 頻繁 → 分ける
  2. 違いは構造上のものですか、それとも意味上のものですか?

    • 構造は同じで、意味が異なる → 統合を検討する
    • 構造が異なる → 分けたままにする
  3. 統合すると重要な型安全性が失われますか?

    • はい → 分けたままにする
    • いいえ → 統合する

実例:イベント

// 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:「違いをモデル化するより、型を統合する」より。

コメント