TypeScriptの「Intersection Types(交差型)」で、関数の引数を美しく合成する極意
こんにちは。TypeScriptの世界へようこそ。
日々コードを書いていると、「この関数、Aの機能もBの機能も必要なんだけど、引数がどんどん長くなっていく…」と悩むことはありませんか?
今回は、TypeScriptの型システムの中でも非常に強力な武器である「Intersection Types(交差型:`&`)」を使って、複数のインターフェースをエレガントに合成する手法を解説します。ここをマスターすると、コードの再利用性が劇的に向上し、「散らかった引数」から卒業できますよ。
—
そもそも「交差型 (&)」とは何か?
数学の集合演算で言えば、「積集合」に相当します。
`A & B` と書くと、「Aの性質」と「Bの性質」の両方を併せ持つ型が誕生します。
イメージしてみてください。
- インターフェースA: `name: string` を持つもの(名前がある)
- インターフェースB: `age: number` を持つもの(年齢がある)
これらを `A & B` で合成すると、「名前と年齢の両方を持つ」新しい型が出来上がります。
interface User {
name: string;
}
interface Age {
age: number;
}
// 交差型を使って合成する
type UserProfile = User & Age;
const myProfile: UserProfile = {
name: “Taro”,
age: 25,
};
「なんだ、ただの継承(extends)と何が違うの?」と思われるかもしれませんね。実は、関数の引数においてこの柔軟性が真価を発揮するのです。
—
実践:複数の責務を合成する関数設計
現場の開発では、特定の機能だけを抽出した小さなインターフェースを定義し、必要に応じてそれらを「合体」させることで、疎結合な設計を目指します。
コード例:ユーザー登録と権限付与の合成
例えば、「ユーザー情報」を扱う処理と「ログ記録」を行う処理を分けたい場合を考えてみましょう。
interface UserData {
id: string;
email: string;
}
interface Logger {
logAction: (action: string) => void;
}
// ユーザーデータとロガーを引数で合体させる
function registerUser(data: UserData & Logger) {
// ユーザーの登録処理…
console.log(`Registering user: ${data.id}`);
// ログ出力処理…
data.logAction(“User registration started”);
}
// 使用例
const user = {
id: “uid_001”,
email: “test@example.com”,
logAction: (action: string) => console.log(`[LOG]: ${action}`)
};
registerUser(user); // 見事に両方の機能が統合されています
このように引数を合成すると、「UserDataを必要とする処理」と「Loggerを必要とする処理」を、関数側で強制的にマージできるため、責務の分離が非常に明確になります。
—
初学者が陥りやすい「罠」と解決策
交差型を使う際、よく遭遇するエラーや勘違いがあります。ここをクリアすれば、もうTypeScriptの型システムで怖気づくことはありません。
1. プロパティの競合(型が矛盾する場合)
もし、全く同じ名前のプロパティで型が違っていたらどうなるでしょうか?
interface A { prop: string; }
interface B { prop: number; }
type C = A & B;
// Cの ‘prop’ は ‘string & number’ となります。
// つまり ‘never’ (決して存在しない型) になります。
`string` であり、かつ `number` である値は存在しませんよね。結果として、そのプロパティには何も代入できなくなります。「合成するインターフェース同士でプロパティ名が被っていないか」は常に意識してください。
2. オプショナル引数との組み合わせ
関数引数にオプショナルな要素を含める場合、少し注意が必要です。
interface Base { id: string; }
interface Options { debug?: boolean; }
function process(input: Base & Options) {
// input.debug は boolean | undefined になるので、
// 使用時には if (input.debug) のようにチェックが必要です
if (input.debug) {
console.log(“Debug mode on”);
}
}
「引数として渡す側」は、`Options` がオプショナルなので、`debug` プロパティを省略可能です。しかし、関数内部では `undefined` の可能性を考慮しなければなりません。これがTypeScriptの安全性の正体です。
—
先輩からのアドバイス:なぜインターフェースを合成するのか?
多くの初学者は、「なぜわざわざ型を分けて合成するの?最初から一つにまとめればいいじゃないか」と考えがちです。
ですが、「機能の最小単位(Atomic)」でインターフェースを定義することは、大規模開発における「変更容易性」に直結します。
- `Logger` だけを他の関数でも使い回したいとき、最初から一つにまとめていたら、余計な `UserData` まで持ち回らなければなりません。
「必要な機能だけを、必要な場所で合体させる」
これが、TypeScriptで設計を掌握するための極意です。
—
まとめ:TypeScriptを掌握するステップ
1. 小さなインターフェースを作る:一つの責務に集中させる。
2. `&` で合成する:関数の引数で、その時に必要な能力だけを組み合わせる。
3. 矛盾を避ける:プロパティ名の競合には細心の注意を払う。
このテクニックを身につければ、あなたのコードはより柔軟に、そして何より「型安全」に守られるようになります。最初は難しく感じるかもしれませんが、この「型を組み立てる」感覚こそが、TypeScriptの醍醐味です。
さあ、次はどんなインターフェースを合成してみますか?応援していますよ!