【入門編】引数に渡すオブジェクトの「余剰プロパティ」を型安全に弾くための設計 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!フルスタックエンジニアの先輩です。
TypeScriptの学習、順調に進んでいますか?

JavaScriptからTypeScriptへ移行したばかりの頃や、C#やJavaといった他の厳密な型を持つ言語からやってきた開発者が、必ずと言っていいほど最初に「おや?」とつまずくポイントがあります。それが「オブジェクトの余剰プロパティ問題」です。

構造的部分型(Structural Subtyping)というTypeScriptの根本的な思想が生み出すこの挙動を正しく理解すると、コードの安全性と開発体験が劇的に跳ね上がります。

今回は、引数に渡すオブジェクトの余分なプロパティを型安全にバシッと弾くための設計手法について、本質から分かりやすく紐解いていきましょう。ここをクリアすれば、TypeScriptの型システムとの距離がぐっと縮まりますよ!

—

1. なぜ「余分なプロパティ」が渡せてしまうのか?(TypeScriptの基本思想)

まずは、よくあるシチュエーションを見てみましょう。ユーザー情報を登録する関数を作るとします。

type User = {
name: string;
age: number;
};

function registerUser(user: User) {
console.log(`登録名: ${user.name}, 年齢: ${user.age}`);
}

// これは当然エラーになりません
registerUser({ name: “Alice”, age: 28 });

ここまでは完璧ですね。では、次のようなコードを書いたとき、TypeScriptはどのように反応すると思いますか?

// あろうことか、定義されていない ‘role’ プロパティを混ぜてみる
registerUser({ name: “Bob”, age: 30, role: “admin” });

他の静的型付け言語(JavaやC#など)に慣れていると、「`User` 型に `role` なんて定義されてないんだから、コンパイルエラーになるはず!」と思われるでしょう。

しかし、TypeScriptでこれを実行すると……なんと、エラーにならずにすんなり通ってしまいます。(※直にオブジェクトリテラルを渡す場合を除き、変数経由だと話が変わるのですが、ここは後ほど詳しく解説します)

なぜ、TypeScriptは余計なプロパティを見逃してくれるのでしょうか?

構造的部分型(ダック・タイピング)の正体

TypeScriptは「構造的部分型」を採用しています。これは、「その箱に、必要な中身(プロパティ)さえ入っていれば、他に何が入っていても(余分なものがあっても)同じものとみなす」という思想です。

例えるなら、「『りんごとみかんが入った袋』を持ってきて」と言われたとき、りんごとみかんが入っていれば、ついでにバナナが入っていようが「条件クリア!」とみなすのがTypeScriptの世界です。

実務の現場(特にAPIのレスポンスなどを扱う場合)では、この緩さが「余分なプロパティがあってもそのまま受け流せる」という強力な柔軟性になります。しかし、厳密なバリデーションを行いたい関数の引数などでは、これが「タイポ(打ち間違い)の検知漏れ」というバグの原因になってしまいます。

// ほんとうは ‘age’ と書きたかったのに、うっかり ‘ag’ と打ち間違えたとする
// もし型に ‘ag’ が許容されてしまっていたら……?

(※実際にはタイポしたプロパティ単体であれば型エラーになりますが、意図しない追加プロパティが混ざるリスクは残ります)

この「緩さ」をコントロールし、必要に応じて「余分なプロパティを一切許さない(厳格なチェックを行う)」ようにするにはどうすればよいのでしょうか?

—

2. 直にオブジェクトを渡すときの「特別ルール」

ここで、TypeScriptのコンパイラが持つ非常にユニークな挙動を知っておく必要があります。

先ほど、「直にオブジェクトリテラルを渡すとエラーになる場合がある」と言いました。実は、変数に代入するかどうかで、TypeScriptのチェックの厳しさが変わるのです。

type User = {
name: string;
age: number;
};

function registerUser(user: User) {
/ 略 /
}

// パターンA:変数に一度入れてから渡す
const inputData = { name: “Charlie”, age: 25, role: “admin” };
registerUser(inputData); // ← ❌ エラーにならない!

あれ? パターンAでは `role` が含まれているのにエラーになりません。
しかし、こう書くとどうでしょう。

// パターンB:関数の引数に直接オブジェクトを書き込む(オブジェクトリテラル)
registerUser({ name: “Charlie”, age: 25, role: “admin” }); // ← 🔴 ここでエラーになる!

おや? パターンBでは、TypeScriptのコンパイラが怒り出します。

> 型 ‘{ name: string; age: number; role: string; }’ の引数を型 ‘User’ のパラメータに割り当てることはできません。
> オブジェクト リテラルには既知のプロパティのみを指定できますが、’role’ は ‘User’ に存在しません。

これが、TypeScriptの「オブジェクトリテラルに対する余剰プロパティチェック(Excess Property Checking)」と呼ばれる機能です。TypeScriptは、開発者が関数に直接オブジェクトを書き込んだ場合に限り、「おや、タイポして余計なプロパティが混ざっているのではないか?」と疑い、厳しく弾いてくれるのです。

—

3. 変数経由でも余剰プロパティを完全に封殺する設計テクニック

「なるほど、直接書けば弾いてくれるんだね。じゃあ安心だ!」……とはいきません。実際の開発では、APIから取得したデータや、別の場所で作った設定オブジェクトなど、「一度変数に入れてから関数に渡すケース」が圧倒的に多いからです。

変数経由であっても、「余分なプロパティが混ざっていたらコンパイル時に絶対許したくない!」という場面において、プロ仕様の設計テクニックを2つご紹介します。

テクニック①:厳密な型ガード関数や「Never型」を利用したアプローチ

もっともプリミティブかつ確実な方法の1つは、受け取る側の型で「余分なプロパティに `never` を指定する」というテクニックです。

type User = {
name: string;
age: number;
};

// 「User以外のプロパティが含まれていたら受け付けない」という厳格な型を作る
// Mapped Types と keyof を組み合わせた高度なテクニックです
type StrictUser = T extends User
? keyof T extends keyof User
? User
: “Error: 許可されていないプロパティが含まれています”
: never;

// これを利用して関数をラップする
function registerUserStrict(user: StrictUser) {
console.log(user);
}

const inputData = { name: “Dave”, age: 40, role: “admin” };

// 変数経由であっても、余分な ‘role’ があるためコンパイルエラーにできる!
// registerUserStrict(inputData);

少し難しく見えますが、要するに「定義されたプロパティ以外の侵入を型レベルでブロックする網」を張るイメージです。

テクニック②:Mapped Typesによる厳格なキー制限(推奨)

もう少し実用的なアプローチとして、ジェネリクスを活用して「渡されたオブジェクトのキーが、定義された型に完全に一致しているか」を強制する方法があります。

// 厳密にプロパティの過不足をチェックするヘルパー型
type Exact = T & {
[K in keyof T]: K extends keyof Shape ? Shape[K] : never;
};

type User = {
name: string;
age: number;
};

// 引数の型を Exact に指定する
function registerUserExact(user: Exact) {
console.log(`登録名: ${user.name}, 年齢: ${user.age}`);
}

// 1. 正しいケース
registerUserExact({ name: “Eve”, age: 22 }); // OK

// 2. 変数経由で余分なプロパティを入れたケース
const rawData = { name: “Frank”, age: 35, role: “guest” };

// 型 ‘Exact<{ name: string; age: number; role: string; }, User>‘ の引数を割り当てようとしてエラーになる!
// registerUserExact(rawData);

この `Exact` パターンを覚えておくと、「構造的部分型の緩さ」を完全に手懐けることができます。外部から渡ってくるデータの型安全性を極限まで高めたいときに、非常に強力な武器になります。

—

4. まとめ:型システムの「意図」を理解してコードを美しく保とう

今回は、TypeScriptの構造的部分型が生み出す「余剰プロパティ」の挙動と、それを厳密に制御するための設計手法について解説しました。

  • TypeScriptの基本は「構造的部分型」:必要なプロパティさえあれば、余分なものがあっても受け入れるのがデフォルト。
  • オブジェクトリテラルには自動チェックが働く:関数の引数に直接書いた場合は、コンパイラが余剰プロパティを検知してエラーにしてくれる。
  • 変数経由でも厳格に弾きたいときはジェネリクスやMapped Typesを活用する:必要に応じて `Exact` 型などのガードを組み込み、型の安全地帯を構築する。

TypeScriptの型システムは、私たち開発者を縛り付けるためのものではなく、「未来のバグやタイポからコードを守るための最強の防壁」です。

この仕組みの裏側にある「型評価のルール」を腑に落とせば、TypeScriptを書く手がもっと楽しく、もっと自信に満ちたものになりますよ。

それでは、次のステップでも一緒にレベルアップしていきましょう!

タイトルとURLをコピーしました