【入門編】引数に渡すオブジェクトの「余剰プロパティ」を型レベルで警告・禁止する設計 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!フロントエンドからNode.jsまで、TypeScriptのことなら何でも聞いてくださいね。

今回は、TypeScriptの学習者が必ず一度はハマり、そして型システムの奥深さに感動するテーマ「引数に渡すオブジェクトの余剰プロパティを型レベルで警告・禁止する設計」についてお話しします。

ここをクリアすると、TypeScriptの「構造的部分型付け(Structural Subtyping)」の本質が手に取るようにわかるようになりますよ。さっそく一緒に見ていきましょう!

—

1. なぜ「余剰プロパティ」で困ることがあるのか?

TypeScriptは「構造的部分型付け」という強力な仕組みを採用しています。これは、「必要なプロパティさえ持っていれば、余分なプロパティが入っていてもOK(それを許容する)」という哲学です。

例えば、次のようなコードを考えてみましょう。

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

function printUser(user: User) {
console.log(`${user.name} (${user.age}歳)`);
}

// 正常な呼び出し
printUser({ name: ‘Taro’, age: 25 });

ここまでは順調ですよね。では、もしうっかりスペルミスをして、`age` の代わりに `ege` というプロパティを渡してしまったらどうなるでしょうか?

// 😱 うっかりミス! “age” ではなく “ege” と書いてしまった
printUser({ name: ‘Hanako’, ege: 30 });

他の厳密な言語(JavaやC#など)であれば、ここで即座にコンパイルエラーになります。しかし、通常の変数に代入せず、関数に直接オブジェクトリテラルを渡す場合(Freshness / 对象的新鮮さ)、TypeScriptは特別な「余剰プロパティチェック」を働かせます。

実は、このケースではTypeScriptは賢く「`ege` なんてプロパティは `User` 型に定義されていないよ!」とエラーを出してくれます。

じゃあ、何が問題なの?

問題は、「一度変数に受けてから渡す場合」です。

// 一度別の変数に格納する
const inputData = {
name: ‘Jiro’,
ege: 25, // スペルミス!
gender: ‘male’, // 余計なプロパティ
};

// この呼び出しは… エラーにならない!?
printUser(inputData);

なんと、このコードはエラーになりません。
なぜなら、`inputData` という変数を経由した時点で、TypeScriptは「構造的部分型(`name` と `age` さえあれば、他に何が入っていても `User` として扱っていいよ)」のルールを適用し、余分なプロパティを目をつぶってスルーしてしまうからです。

APIのレスポンスやフォームの入力値を受け取る際、こうしたタイポ(入力ミス)や予期せぬゴミデータが素通りしてしまうと、後続の処理で思わぬバグを生む原因になりますよね。

「変数を経由しようがしまいが、厳密に許可されたプロパティ以外は一歩も通したくない!」
それを実現するのが、今回のテーマである余剰プロパティの完全ブロック設計です。

—

2. 解決策:ジェネリクスと `Record` を使った厳密な型制限

では、どうすればこの「すり抜け」を防げるのでしょうか?
結論から言うと、関数側をジェネリクス(総称型)を使いつつ、許可されていないキーが混ざった瞬間に型エラーになるような「おもり」を仕掛けます。

実際のコードを見てみましょう。

// 1. 基本となる型定義
type User = {
name: string;
age: number;
};

// 2. 余剰プロパティを完全に禁止するスマートな関数シグネチャ
function printUserStrict(
user: T & Record, never>
) {
console.log(`${user.name} (${user.age}歳)`);
}

なんだか見慣れない記号が並んでいて難しく見えますか?
大丈夫です。プログラミングの呪文のように見えますが、やっていることは非常に理にかなっています。少しずつ分解して解剖していきましょう。

コードの心臓部を分解して理解する

1. ``

  • 呼び出し元が渡したオブジェクトの型を、そのまま `T` という変数(型変数)としてキャプチャします。ただし、最低限 `User` の構造は満たしている必要があります。

2. `Exclude`

  • 「渡された型 `T` が持つキー」から「本来許可されている `User` のキー」を引き算します。
  • 例:もし `T` に `ege` や `gender` が含まれていれば、この引き算の結果、`”ege” | “gender”` という余分なキーのUnion型が残ります。

3. `Record<..., never>`

  • 余分なキーが存在する場合、その値の型を `never`(=絶対に存在し得ない、代入不可能な型)に強制指定します。

4. `T & …`

  • 本来の `T` と、「余分なキーの型が `never` になったもの」を交差(Intersection)させます。
  • もし余分なキーに値を入れようものなら、その値の型が `never` になるため、「そんな値は存在しない(代入できない)」というコンパイルエラーが爆誕する仕組みです。

—

3. 実践!エラーがどう出るか試してみよう

この `printUserStrict` 関数を使って、実際に挙動を確認してみましょう。

// 正常系:余計なプロパティがない場合
printUserStrict({
name: ‘Taro’,
age: 25,
}); // ✅ コンパイル成功!

// 異常系1:直接リテラルで余分なプロパティを渡した場合
printUserStrict({
name: ‘Hanako’,
age: 30,
gender: ‘female’, // ❌ ここでコンパイルエラー!
});

// 異常系2:一度変数に受けてから渡した場合(ここが重要!)
const rawInput = {
name: ‘Jiro’,
age: 20,
ege: 22, // スペルミス!
};

printUserStrict(rawInput);
// ❌ なんと!変数経由であっても、”ege” が含まれているためバッチリコンパイルエラーになります!

変数を経由しようが、APIから受け取ったオブジェクトをそのまま流し込もうが、「定義されていないプロパティ」が紛れ込んでいれば、TypeScriptのコンパイラが容赦なく赤線を引いて教えてくれます。これぞ、アーキテクトが好む堅牢な型設計です。

—

4. 陥りやすい文法エラーと注意点

ここで、「よし、全部の関数にこの書き方を適用しよう!」と思った方、少しだけ立ち止まってくださいね。このテクニックには、実務で使う上で知っておくべきトレードオフ(注意点)があります。

エラーメッセージが難解になる

TypeScriptの型エラーに慣れていないメンバーがいるチームの場合、`Record, never>` が引き起こすエラーメッセージ(例:Type ‘string’ is not assignable to type ‘never’. など)を見たときに、「何が起きているのか分からない!」とパニックになってしまうことがあります。

拡張性とのトレードオフ

もし将来的に、「この関数は、ベースの型に加えて、任意の追加メタデータ(`[key: string]: any`)を受け取れるようにしたい」といった要件が出てきた場合、この厳密なチェックは邪魔になってしまいます。
「厳密に型安全にしたい部分(ドメインモデルの入力値など)」と「柔軟性を残したい部分」をしっかりと見極めて使い分けるのが、シニアエンジニアの腕の見せ所です。

—

まとめ

今回は、TypeScriptの構造的部分型付けの特性を逆手に取り、引数の余剰プロパティを型レベルで完全封鎖するテクニックを解説しました。

  • 通常のオブジェクトリテラルは自動でチェックされるが、変数を経由するとチェックがすり抜ける。
  • ジェネリクス(`T`)と `Exclude` / `never` を組み合わせることで、変数経由であっても余剰プロパティを型レベルで検知・禁止できる。
  • 堅牢性を高めたいドメインロジックの境界線(API入力やフォームバリデーション後など)で非常に強力な武器になる。

ここをクリアできれば、TypeScriptの型システムを「使わされている状態」から「手足のように操る状態」へ一歩ステップアップできますよ。ぜひ、あなたのプロジェクトの厳格なバリデーションが必要な箇所で試してみてくださいね!

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