こんにちは。テクニカルリードの私だ。
今日のコードレビューで、またこんな「思考停止した型定義」を見かけた。
// 良くあるアンチパターン
interface User {
id: string;
name: string;
email: string;
age: number;
createdAt: Date;
updatedAt: Date;
// …ほか20個のプロパティ
}
function displayUserName(user: User): string {
return user.name;
}
おいおい、待ってくれ。たった `name` プロパティを1つ表示したいがために、なぜ肥大化した `User` インターフェース全体を引数に強制するのか? これではコンポーネントや関数が特定のドメインモデルに強く結合し、テストのたびに不必要なモックデータを大量に用意する地獄を生み出すことになる。
TypeScriptの心臓部は 構造的型付け(Structural Typing / ダックタイピング) だ。「何であるか(Nominal)」ではなく、「どう振る舞うか(Shape)」で型が決まる。この言語の性質を理解し、関数の引数を極限までスリムに保つテクニックを授けよう。
—
1. なぜ「過剰な型定義」は悪なのか?
実務の現場において、巨大なインターフェースを関数の引数に指定し続けると、以下の3重苦に見舞われる。
1. 結合度の爆発: 関数が「特定のエンティティ」に依存するため、似た構造を持つ別のデータソース(例えば `Admin` や `Guest`)を流用できなくなる。
2. テストの困難さ: わずか数個のプロパティしか使わないのに、必須プロパティをすべて満たすモックオブジェクトを作らなければテストが通らない。
3. リファクタリング耐性の低下: `User` 型の無関係なプロパティ(例: `updatedAt`)の型を変更しただけで、それを直接描画していないはずの純粋なUIコンポーネントまでコンパイルエラーの波及を受ける。
TypeScriptのコンパイラは、私たちが渡したオブジェクトが「関数が要求する最小限の構造(Shape)」を満たしているかを静的に検証してくれる。型定義の粒度を「関数が必要とする最小単位」まで切り詰めるべきだ。
—
2. 現場で使える!Structural Typingを極めた設計パターン
では、実際のプロダクションコードをベースに、美しく堅牢な設計を見ていこう。
以下の例では、APIから取得した複雑なユーザー情報から、特定のUIパーツやフォーマッターに必要なプロパティだけを安全かつ柔軟に抽出して処理するパターンを実装している。
/
- 肥大化したドメインモデル(APIレスポンスの元型)
/
interface UserEntity {
id: string;
firstName: string;
lastName: string;
email: string;
hashedPassword: string;
roles: string[];
lastLoginAt: Date;
createdAt: Date;
updatedAt: Date;
}
/
- 【アンチパターン】
- ユーザーのフルネームを作るために UserEntity 全体を要求している。
- これでは他の「名前を持つエンティティ(例: Organization)」で再利用できない。
/
// function formatFullNameNG(user: UserEntity): string {
// return `${user.firstName} ${user.lastName}`;
// }
/
- 【正しいアプローチ:Structural Typingの活用】
- この関数にとって必要なのは “firstName” と “lastName” を持つことだけだ。
- インライン型あるいは専用の最小限のインターフェースを定義する。
/
type HasName = {
firstName: string;
lastName: string;
};
export function formatFullName(target: HasName): string {
// コンパイラは target が HasName の構造を持つことだけを検証する
return `${target.firstName} ${target.lastName}`;
}
/
- さらに一歩進んだ、コンポーネント指向のプロパティ抽出パターン
- Reactなどのフロントエンド開発で非常に強力に機能する
/
type UserAvatarProps = {
// 必要なプロパティだけをピックアップ、あるいは直接定義
firstName: string;
lastName: string;
avatarUrl?: string; // オプショナルも構造の一部
};
export function renderUserAvatarSummary(user: UserAvatarProps): string {
const name = formatFullName(user); // HasName の要件を満たしているためそのまま渡せる
const avatar = user.avatarUrl ?? ‘/default-avatar.png’;
return `[${avatar}] ${name}`;
}
// — 使用例 —
const rawUser: UserEntity = {
id: ‘usr_001’,
firstName: ‘Taro’,
lastName: ‘Yamada’,
email: ‘taro@example.com’,
hashedPassword: ‘secr3t_hash’,
roles: [‘admin’],
lastLoginAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
// 1. UserEntity をそのまま渡しても、構造的型付けにより問題なくコンパイル・実行される
console.log(formatFullName(rawUser)); // “Taro Yamada”
// 2. まったく別のオブジェクトであっても、必要な構造を満たしていれば関数に渡せる!
const tenant = {
id: ‘tenant_001’,
firstName: ‘Acme’,
lastName: ‘Corp’,
taxId: ‘TX-999’,
};
console.log(formatFullName(tenant)); // “Acme Corp” (再利用性の証明)
なぜこれが美しいのか?
`formatFullName` や `renderUserAvatarSummary` は、もはや `UserEntity` の存在すら知る必要がない。ドメインの変更(例えば `UserEntity` に新しいフィールドが追加・削除されたり)があっても、この関数の入力構造が変わらない限り、影響を受けることはゼロだ。これが疎結合な型設計である。
—
3. 構造的型付けの「限界」と、実務での注意点
万能に見える構造的型付けだが、TypeScriptのコンパイラ挙動において気をつけておくべき「罠」がいくつかある。テクニカルリードとして、その限界と回避策も共有しておこう。
① 余剰プロパティチェック(Excess Property Checks)の壁
オブジェクトリテラルを直接関数の引数に渡す場合、TypeScriptは厳格な「余剰プロパティチェック」を行う。定義されていないプロパティが混ざっていると、たとえ構造的型付けの原則上は許容範囲(部分型関係)であってもエラーになる。
type Point2D = { x: number; y: number };
function printPoint(p: Point2D) {
console.log(`X: ${p.x}, Y: ${p.y}`);
}
// OK
printPoint({ x: 10, y: 20 });
// 【コンパイルエラー】 Object literal may only specify known properties
// printPoint({ x: 10, y: 20, z: 30 });
対策: 変数経由で渡す(間接参照にする)か、余分なプロパティを許容したい場合は型定義側でインデックスシグネチャ (`[key: string]: unknown`) を検討する(ただし型の安全性が落ちる諸刃の剣なので多用は禁物)。
② プリミティブ臭いオブジェクト(Structural Typingの暴走)
すべての型を構造だけで解決しようとすると、構造が全く同じ「別物」を誤って混入させるバグを防げなくなる(例: `UserId` と `ProductId` がどちらも単なる `{ id: string }` である場合)。
これには Branded Types(ブランド型) を組み合わせて、構造の柔軟性と公称型(Nominal Typing)の安全性を両立させるアプローチが必要になる。
// ブランド型によるプリミティブの型安全化の例
type Brand
type UserId = Brand
type ProductId = Brand
// これらを混同させない設計と構造的型付けを適材適所で使い分ける
—
4. 本日のまとめ
1. 全体を渡すな、必要な部品だけを要求しろ: 巨大なエンティティ型を関数の引数にするのをやめ、必要なプロパティだけを切り出したミニマルな型(あるいはインライン型)を定義する。
2. 構造的型付けを武器にせよ: 構造さえ合っていればどのオブジェクトでも受け入れられるため、コードの再利用性とモジュール性が劇的に向上する。
3. 影響範囲を局所化せよ: 型の依存関係を最小限に抑えることで、大規模アプリケーションにおけるリファクタリングのコストを最小化できる。
コードレビューで巨大な型が引数に並んでいるのを見つけたら、こう問いかけてほしい。
「おい、この関数は本当にそのモデル全体のことを知る必要があるのか?」と。
さあ、型定義をスリムに削ぎ落とし、コンパイラのポテンシャルを極限まで引き出した美しいコードを書こう。