dry-types について
型操作やジェネリクスを使って型定義の重複を減らす、TypeScript 開発向けのスキルです。関連する型の同期を保ちつつ、構造が似ているだけの型を抽象化しない方法を扱います。
主な機能
- 01`extends` で型を拡張
- 02`Pick` と `Omit` でプロパティを選択
- 03`Partial`、`keyof`、`typeof` を活用
- 04マップ型やユーティリティ型で型を導出
- 05意味的に関連する型だけを抽象化
活用シーン
- 01大きな型から部分型を導出
- 02関連する型定義の同期を維持
- 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「型操作とジェネリック型を使って繰り返しを避ける」に基づいています。