【入門編】TypeScriptのオブジェクト型における「余剰プロパティチェック」の挙動と制限 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムの世界へようこそ。
日々フロントエンドやNode.jsの開発でTypeScriptを書いていると、最初は「おっ、賢いな」と感動し、時には「えっ、どうしてここでエラーになるの!?」と頭を抱える瞬間に出会うことありませんか?

その中でも、多くの開発者が最初に直面し、そして深く納得するまで少し戸惑うのが「余剰プロパティチェック(Excess Property Checking)」という挙動です。

「あれ、さっきはエラーにならなかったのに、なんで今回は怒られるの?」
そんな疑問を持ったことがあるなら、ここをクリアすればTypeScriptの型システムの本質がグッと見えてきますよ。今日は、優しく丁寧に、その裏側の仕組みまで紐解いていきましょう!

—

1. 最初に結論:オブジェクトリテラルと変数の違い

まずは、よくあるこんなコードを見てください。

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

// パターンA:関数に直接オブジェクトを書く(オブジェクトリテラル)
function printUser(user: User) {
console.log(`Name: ${user.name}, Age: ${user.age}`);
}

// ここで赤波線(コンパイルエラー)が出る!
printUser({ name: “Taro”, age: 25, role: “admin” });
// ❌ Error: Object literal may only specify known properties, and ‘role’ does not exist in type ‘User’.

「おや?」と思いますよね。`User`型には `name` と `age` があればいいはずなのに、余分な `role` を入れたら、TypeScriptが「そんなプロパティは知らん!」と怒ってきました。

では、次を見てください。

// パターンB:いったん変数に入れてから渡す
const inputData = { name: “Hanako”, age: 28, role: “admin” };

printUser(inputData);
// ⭕️ なぜかエラーにならない!すんなり通る!

……不思議だと思いませんか?
同じ中身(`name`, `age`, `role`)を持つオブジェクトなのに、「直接渡す(パターンA)」とエラーになり、「変数に入れる(パターンB)」とエラーにならない。

この挙動の違いを生み出しているのが、TypeScriptのコンパイラが持つ特別な仕組み「余剰プロパティチェック」なのです。

—

2. なぜこの挙動の違いが存在するのか?(型システムの思想)

「TypeScriptって、構造的型付け(Structural Subtyping)なんじゃないの?」
その通りです。TypeScriptは基本的に「形が合っていればOK」という思想で動きます。パターンBがエラーにならないのは、まさにこの構造的型付けのルール(代入可能性)が働いているからです。

変数 `inputData` は、TypeScriptによって次のように推論されます。
`(property) name: string`, `(property) age: number`, `(property) role: string`

これを `User` 型を受け取る関数に渡すとき、TypeScriptはこう考えます。
「`User` 型が求めている `name` と `age` はちゃん持っているな。余分な `role` はあるけれど、`User` 側が無視すれば実害はないからOK(部分型の関係)」

では、なぜパターンA(直接渡す場合)では厳しくチェックされるのでしょうか?

許されざる「タイポ(打ち間違い)」を防ぐため

もしパターンAでエラーにならなかったらどうなるでしょう? 実際の開発現場を想像してみてください。

type Options = {
useCache: boolean;
timeout: number;
};

function connect(options: Options) {
// …
}

// ユーザーがうっかり “timeout” を “timout” と打ち間違えたとする
connect({
useCache: true,
timout: 1000, // 惜しい! ‘e’ が抜けている
});

もし余剰プロパティチェックがなかったら、このコードは何のエラーも出さずにスルーしてしまいます。結果として、設定したはずのタイムアウトは適用されず、デフォルト値が使われてバグの原因になりますよね。

オブジェクトリテラルを直接書くということは、多くの場合「この関数専用に、その場で設定オブジェクトを組み立てている」瞬間です。だからこそTypeScriptのコンパイラは、「おいおい、定義されていないプロパティがあるぞ。タイポじゃないのか?」と親切心(あるいは厳格さ)からストップをかけてくれているのです。

—

3. 図解:頭の中のメモリ・型評価イメージ

コンパイル時に、TypeScriptの頭の中では次のような判断が行われています。

[ オブジェクトリテラルを直接渡す ]
コード: printUser({ name: “Taro”, age: 25, role: “admin” })
│
▼
[ 厳格モード発動:余剰プロパティチェック ]
“User型に定義されていない ‘role’ が直書きされている!”
│
▼
❌ コンパイルエラー! (タイポの可能性を検知)

[ 変数経由で渡す ]
コード: const inputData = { name: “Hanako”, age: 28, role: “admin” };
│
▼
[ 通常の構造的型付け(部分型チェック) ]
“inputDataは User が必要とするプロパティを満たしているか?” -> 満たしている
│
▼
⭕️ コンパイル通過! (余分なプロパティは無視される)

この違いを腑に落ちて理解できると、TypeScriptのコードを書くときの「迷い」が劇的に減ります。

—

4. それでも余分なプロパティを受け入れたい時のスマートな対処法

「APIから返ってきたレスポンスオブジェクトをそのまま渡したい」など、余分なプロパティが存在することが前提のケースもありますよね。そんなときはどうすればよいでしょうか?

対処法1:型定義に「インデックスシグネチャ」を持たせる

もしオブジェクトが、追加の任意のプロパティを持つことを許容する設計であれば、型定義側でそれを許すことができます。

type FlexibleUser = {
name: string;
age: number;
[key: string]: any; // どんなプロパティの追加も許可する
};

function printFlexibleUser(user: FlexibleUser) {
// …
}

// これなら直接書いてもエラーにならない!
printFlexibleUser({ name: “Jiro”, age: 30, role: “admin”, extra: “foo” });

対処法2:一度変数に受ける(または型アサーションを使う)

あらかじめ変数に切り出すか、どうしてもその場で直書きしたい場合は `as` を使って型を明示(アサーション)することで、余剰プロパティチェックをバイパスできます。

// 型アサーションで「これはUser型として扱う」とコンパイラに伝える
printUser({ name: “Saburo”, age: 22, role: “admin” } as User);
// ⭕️ エラー回避(ただしタイポの検知責任は開発者側になります)

—

まとめ

いかがでしたでしょうか?

  • オブジェクトリテラルを直接渡す場合:

タイポや意図しないプロパティの混入を防ぐため、「余剰プロパティチェック」が厳格に働き、型にないプロパティがあるとエラーになる。

  • 変数経由で渡す場合:

通常の構造的型付け(部分型チェック)が適用され、必要なプロパティさえ満たしていれば余分なプロパティがあってもエラーにならない。

TypeScriptの型エラーに出会ったとき、「なぜ今、コンパイラはこのチェックをしているのか?」というその意図(背景)に思いを馳せると、型エラーは煩わしい障害ではなく、頼もしい相棒に変わっていきます。

ここをクリアしたあなたなら、もうTypeScriptの型システムの基本はバッチリマスターできていますよ!自信を持って次のコードを書いていきましょう。

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