mcpField
D
dry-types

dry-types

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

dry-types について

型操作やジェネリクスを使って型定義の重複を減らす、TypeScript 開発向けのスキルです。関連する型の同期を保ちつつ、構造が似ているだけの型を抽象化しない方法を扱います。

主な機能

  1. 01`extends` で型を拡張
  2. 02`Pick` と `Omit` でプロパティを選択
  3. 03`Partial`、`keyof`、`typeof` を活用
  4. 04マップ型やユーティリティ型で型を導出
  5. 05意味的に関連する型だけを抽象化

活用シーン

  1. 01大きな型から部分型を導出
  2. 02関連する型定義の同期を維持
  3. 03オプション型や拡張型の作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill dry-types

インストール後、Claude Code で/dry-typesを入力して読み込まれたか確認します。

詳細

型操作とジェネリック型を使って繰り返しを避ける

概要

DRY(Don't Repeat Yourself:同じことを繰り返さない)をコードだけでなく、型にも適用しましょう。

型の重複はコードの重複と同じ問題を引き起こします。一貫性が失われ、保守の負担が増え、バグにつながります。TypeScript の型操作を使って、ほかの型から型を導出しましょう。

このスキルを使う場面

  • インターフェース間でフィールドをコピーしている
  • 複数の型が共通のプロパティを持つ
  • ある型の一部だけを含む型を作りたい
  • 型を自動的に同期させたい
  • 型のオプション版や部分版を作る必要がある

絶対に守るルール

NEVER copy-paste type definitions. Derive types from a single source of truth.

覚えておきましょう:

  • フィールドの追加には extends
  • フィールドの選択には Pick<T, K>
  • フィールドをオプションにするには Partial<T>
  • キーの型を取得するには keyof
  • 値から型を導出するには typeof

問題の見つけ方:型のコピー問題

似た型が異なる方向に進んでいたら、次のような状態です。

// ❌ Duplicated type definitions
interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;   // Duplicated!
  lastName: string;    // Duplicated!
  birth: Date;
}

Person に middleName を追加したらどうなるでしょうか。これらの型は同期しなくなります。

基本的な手法

extends でフィールドを追加する
// ✅ Derive from base type
interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate extends Person {
  birth: Date;
}
Pick でフィールドを選択する
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

// ✅ Select only the fields you need
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
Partial でオプション版を作る
interface Options {
  width: number;
  height: number;
  color: string;
}

class UIWidget {
  constructor(init: Options) { /* ... */ }
  
  // ✅ All fields optional for updates
  update(options: Partial<Options>) { /* ... */ }
}
keyof でキーの型を取得する
type OptionsKeys = keyof Options;
//   ^? type OptionsKeys = "width" | "height" | "color"
typeof で値から型を導出する
const DEFAULTS = {
  width: 640,
  height: 480,
  color: '#00FF00',
};

// ✅ Type derived from value
type Options = typeof DEFAULTS;
//   ^? type Options = { width: number; height: number; color: string; }

標準ライブラリのユーティリティ型

ユーティリティ用途例
Pick<T, K>プロパティを選択Pick<User, 'id' | 'name'>
Omit<T, K>プロパティを削除Omit<User, 'password'>
Partial<T>すべてをオプションにするPartial<Config>
Required<T>すべてを必須にするRequired<PartialConfig>
Readonly<T>すべてを読み取り専用にするReadonly<State>
ReturnType<F>関数の戻り値の型ReturnType<typeof fn>
Parameters<F>関数のパラメーターParameters<typeof fn>

応用パターン

マップ型
// Create optional version manually
type OptionsUpdate = {
  [K in keyof Options]?: Options[K]
};

// Equivalent to Partial<Options>
ユニオン型へのインデックスアクセス
interface SaveAction { type: 'save'; /* ... */ }
interface LoadAction { type: 'load'; /* ... */ }
type Action = SaveAction | LoadAction;

// ✅ Extract discriminant type
type ActionType = Action['type'];
//   ^? type ActionType = "save" | "load"
関数の戻り値に対する ReturnType
function getUserInfo(userId: string) {
  return {
    userId,
    name,
    age,
    // ... many fields
  };
}

// ✅ Derive type from function
type UserInfo = ReturnType<typeof getUserInfo>;
as を使ったキーの再マッピング
interface ShortToLong {
  q: 'search';
  n: 'numberOfResults';
}

// Invert the mapping
type LongToShort = {
  [K in keyof ShortToLong as ShortToLong[K]]: K
};
//   ^? type LongToShort = { search: "q"; numberOfResults: "n"; }

準同型マップ型

keyof を使うと、マップ型は修飾子を保持します。

interface Customer {
  /** How the customer would like to be addressed. */
  title?: string;
  /** Complete name as entered in the system. */
  readonly name: string;
}

// ✅ Preserves optional and readonly
type PickTitle = Pick<Customer, 'title'>;
//   ^? type PickTitle = { title?: string; }

type PickName = Pick<Customer, 'name'>;
//   ^? type PickName = { readonly name: string; }

DRY を適用してはいけない場合

単に偶然似ているだけの型は、切り出さないでください。

// ❌ Don't do this - coincidental similarity
interface NamedAndIdentified {
  id: number;
  name: string;
}

interface Product extends NamedAndIdentified {
  priceDollars: number;
}

interface Customer extends NamedAndIdentified {
  address: string;
}

なぜでしょうか? Product.id と Customer.id は、意味的に異なります。

  • Customer.id は UUID になる可能性があります
  • Product.name と Customer.name は、それぞれ異なる方向に進化する可能性があります

経験則: 意味のある名前を付けられないなら、時期尚早な抽象化である可能性が高いです。

よくあるパターン

ベース型+拡張型
// Shared base
interface Vertebrate {
  weightGrams: number;
  color: string;
  isNocturnal: boolean;
}

// Specific extensions
interface Bird extends Vertebrate {
  wingspanCm: number;
}

interface Mammal extends Vertebrate {
  eatsGardenPlants: boolean;
}
入出力型
// Full type
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

// Input type derived
type CreateUserInput = Pick<User, 'email' | 'name'>;

// Or with Omit
type UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>;
関数シグネチャ
// ✅ Factor out common signatures
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;

const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };

プレッシャーへの対処法

1.「コピー&ペーストのほうが速い」

プレッシャー:「型を複製すればいい。そっちのほうが速い」

回答: 技術的負債が積み重なり、型同士にずれが生じます。

行動: 時間をかけて extends、Pick などを使い、型を導出しましょう。

2.「型は十分に異なる」

プレッシャー:「フィールドが共通しているのは偶然だ」

回答: そのとおりかもしれません。まず、意味的に同じかを確認しましょう。

行動: 同じ概念を表す型だけを切り出しましょう。

危険信号 — 作業を止めて再検討する

  • インターフェースのフィールドをコピー&ペーストしている
  • プロパティのサブセットが同一の型が複数ある
  • ある型を更新したのに、別の型の更新を忘れている
  • ベース型と重複する「...WithX」という名前の型がある

よくある正当化(どれも誤り)

言い訳実態
「フィールドはほんの数個だ」少数のフィールド × 多数の型 = 保守の悪夢
「両方を更新することを覚えておく」忘れるか、チームメイトが忘れます
「導出の仕方が分かりにくい」型のずれのデバッグよりは分かりやすいです

クイックリファレンス

// Adding fields
interface Extended extends Base { newField: T; }

// Selecting fields
type Subset = Pick<Full, 'a' | 'b'>;

// Removing fields  
type WithoutPassword = Omit<User, 'password'>;

// Making optional
type Updates = Partial<Options>;

// Getting return type
type Result = ReturnType<typeof myFunction>;

// Getting key union
type Keys = keyof MyInterface;

要点

型は、信頼できる唯一の情報源から導出しましょう。

TypeScript の型操作(extends、Pick、Partial、keyof、typeof、マップ型)を使って、型同士の関係を表現しましょう。そうすることで型の同期が保たれ、保守の負担が軽減されます。ただし、DRY を適用するのは、型が構造的に似ているだけではなく、意味的に関連している場合に限ります。

参考資料

Dan Vanderkam 著「Effective TypeScript」の項目 15「型操作とジェネリック型を使って繰り返しを避ける」に基づいています。

コメント