bitA Tech Blog
🦆

なぜTypeScriptでは余剰プロパティチェックが行われないケースがあるのか

に公開

🎄 本記事はbitA Tech Blog Advent Calendar 24日目の記事です。

はじめに

この記事では、TypeScriptにおける 余剰プロパティチェック(Excess Property Checks)が

  • 行われるケース / 行われないケース
  • なぜ意図的にそう設計されているのか(原理・採用理由・メリット)

について解説します。

余剰プロパティを設定するとエラーが出るか出ないか

TypeScriptでは、オブジェクト型を定義した際、その型に定義されないプロパティ(余剰プロパティ / Excess Property)をオブジェクトリテラルに含めるとエラーになる場合があります。

// ケース1

interface SquareConfig {
  color: string;
  size: number;
}

const createConfig = (color: string, size: number): SquareConfig => ({
  color,
  size,
  area: size * size // エラー:Object literal may only specify known properties, and 'area' does not exist in type 'SquareConfig'.
})

これは多くの人にとって直感的で、期待通りの挙動でしょう。


しかし、ほぼ一緒の実装でも余剰プロパティの存在が行われない場合もあります!

// ケース2

interface SquareConfig {
  color: string;
  size: number;
}

type CreateConfig = (color: string, size: number) => SquareConfig

const createConfig: CreateConfig = (color, size) => ({
  color,
  size,
  area: size * size // エラーにならない
})

ケース1と一緒で 、SquareConfigには存在しないareaプロパティを定義しているにもかかわらず、エラーは発生しません。

しかし、ここで疑問が生まれます。

  • area があることで SquareConfig に違反しているのでしょうか?
  • そして、color と size を持っているので、事実上 SquareConfig を満たしているとも言えますよね?

確かに、 area は SquareConfig から見ると余剰プロパティですが、余剰プロパティは必ずしも違反として判定されるとは限りません。

つまり、余剰プロパティチェックが行われないケースが存在します。

余剰プロパティチェックが行われるケースと行われないケース

余剰プロパティチェックは特別なケース以外は行われません。公式ドキュメントでは以下のように記載されています。

https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.

訳: オブジェクトリテラルは特別な扱いを受け、他の変数への代入時や引数として渡す際に過剰プロパティチェックが行われます。

ここで余剰プロパティチェックが行われるケースを見てみましょう。

  1. 型付き変数への代入
interface SquareConfig {
  color: string;
  size: number;
}

// 型付き変数への代入のため、チェックされエラーが発生します
const config: SquareConfig = { color: "red", size: 100, area: 10_000 }

const createConfig = (color: string, size: number): SquareConfig => ({
  color,
  size,
  // 関数の返り値への代入のため、チェックされエラーが発生します
  area: size * size // エラー:Object literal may only specify known properties, and 'area' does not exist in type 'SquareConfig'.
})
  1. 関数の引数としてオブジェクトリテラルを渡す場合
interface SquareConfig {
  color: string;
  size: number;
}

const fn = (params: SquareConfig) => { /* do something */ }
fn({ color: "red", size: 100 })
fn({ color: "red", size: 100, area: 10_000 }) // エラー

type CreateConfig = (color: string, size: number) => SquareConfig

const createConfig: CreateConfig = (color, size) => ({
  color,
  size,
  area: size * size // エラーにならない
})

上記以外のケースでは、余剰プロパティチェックは行われません。

前述のエラーが発生しないケース2のコードスニペットでは、関数の戻り値が直接SquareConfigと比較されているわけではありません。

戻り値はまずCreateConfig型の一部として評価され、その後に構造的にSquareConfigと一致するかどうかが判断されます。

この評価過程では、余剰プロパティチェックは行われません。

クイズ:エラーが発生しますか?

ここでクイズです!
以下のコードは余剰プロパティチェックによるエラーは発生するでしょうか?

interface SquareConfig {
  color: string;
  size: number;
}

/* 問題1 */
const config1 = { color: "red", size: 100, area: 10_000 } 
const fn = (params: SquareConfig) => { /* do something */ }
fn(config1)

/* 問題2 */
let config2: { size: number, color: string }
const hoge = { size: 10, color: "red", area: 100 }
config2 = hoge

/* 問題3 */
let config3: { size: number, color: string }
config3 = { size: 10, color: "red", area: 100 }

IDEで確認する前に、ぜひ考えてみてください〜

🦆 TypeScriptと構造的型付け

https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html#structural-type-system

TypeScriptは構造的型付けを採用しています。これは次の例えで説明されることが多い考え方です。

「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない」

class Duck {
  quack() {
    // TODO: 鳴く
  }
  walk() {
    // TODO: 歩く
  }
}

class Swan {
  quack() {
    // TODO: 鳴く
  }
  walk() {
    // TODO: 歩く
  }
  eat() {
    // TODO: 食べる
  }
}

// エラーが出ません。なぜかというと、鳴けるし、歩けるので、これはアヒルでしょう。
const animal: Duck = new Swan();

// もちろん、アヒルとして判定された時点で、アヒルとして定義されていない挙動はできません。
// エラー:Property 'eat' does not exist on type 'Duck'
animal.eat()

Duckが要求する構造(quackとwalk)を満たしているため、SwanはDuckとして扱えます。

SquareConfig のケースを振り返ると、次のようになります:
「SquareConfig に定義されている color と size を持っておけば、SquareConfig として扱ってもいいでしょう」ということです。

型の扱いの条件に、他のプロパティがないかどうかは入りません。

構造的型付けは余剰プロパティチェックを行わないはずでは?

TypeScriptの公式ドキュメントによると、以下のコードについて、次のように記載しています。

However, TypeScript takes the stance that there’s probably a bug in this code.

訳: しかし、TypeScriptはこのコードにバグがある可能性が高いというスタンスを取っています。

interface SquareConfig {
  color?: string;
  width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}
 
let mySquare = createSquare({ colour: "red", width: 100 });

このようなヒューマンエラーを早期に検出ため、追加機能として、特別なシナリオに限って余剰プロパティチェックを行う設計になっています。

なぜTypeScriptは構造的型付けを採用したのか

最大の理由は、JavaScriptとの互換性を優先したいからです。
以下の原理に従うには、構造的型付けの方が自然にフィットします。

  • JavaScriptは元々ダックタイピング
  • 全ての有効なJavaScriptのコードは有効なTypeScriptのコード

構造的型付けを採用することで、副作用として以下のメリットが得られます。

  • 柔軟性 - 明示的な継承やインターフェース実装を書かなくても型が一致する
  • 既存コードとの統合 - サードパーティライブラリや既存のJSコードに型を後付けしやすい
  • 実用性 - オブジェクトリテラルやJSONデータの扱いが簡単になる

これらはすべて、「TypeScriptをJavaScriptの自然な型付き拡張として位置づける」ための設計判断です。

まとめ

この記事を通して、次の点を理解していただければ幸いです。

  • TypeScriptにおいては、余剰プロパティチェックが行われるケースは限定的なケースのみであること
    • 型付きの変数への直接代入
    • 引数としてオブジェクトリテラルを渡す場合
  • TypeScriptにおいて余剰プロパティチェックが行われないのは、構造的型付けシステムを採用したことが理由であること
    • 構造的型付けシステムとダックタイピングの違いについて
  • JavaScriptとの互換性を重視した結果、TypeScriptは構造的型付けシステムを採用していること

クイズの回答
  • 問題1:エラーになりません
  • 問題2:エラーになりません
  • 問題3:エラーになります
bitA Tech Blog
bitA Tech Blog

Discussion