😇

undefinedって2種類あんねん

2023/02/20に公開
8

要約

オブジェクトのプロパティに明示的にundefinedを指定するのと単にプロパティを省略するのでは挙動が異なるので注意しましょう。

本文

以下のようなオプショナルなプロパティをもつ型を定義したとします。

type SomeRecord = {
  foo: number;
  bar?: number;
};

さて、ここで以下のように変数を定義した時、x.bar, y.barの値は同値になるでしょうか?

const hoge1: SomeRecord = {
  foo: 1,
};

const hoge2: SomeRecord = {
  foo: 1,
  bar: undefined,
};

const fuga: SomeRecord = {
  foo: 2,
  bar: 3,
};

const x = { ...fuga, ...hoge1 };
const y = { ...fuga, ...hoge2 };

これは同値にはなりません。コードを実行するとx.bar3になり、y.barundefinedになります。

この挙動はオブジェクトが引数で指定したプロパティを持つか確認する.hasOwnPropertyメソッドを実行してみると謎が解けます。

追記)
コメントでpetamorikenさんから、
プロパティの存在チェックにはObject.hasOwnが推奨されていると教えていただきました。Object.hasOwnを使ってください。

hoge1.hasOwnProperty('bar') // false
hoge2.hasOwnProperty('bar') // true

ご覧の通り、hoge2barプロパティが存在するので、fuga.barが上書きされてしまうのでした。明示的にundefinedを指定しているので当たり前といわれたら確かにそうかもしれません。しかし、TypeScriptを使用する上でこれは結構好ましくない挙動だと私は思います。これはfugaの型定義を修正すると何が不都合かすぐにわかります。

const fuga: Required<SomeRecord> = {
  foo: 2,
  bar: 3,
};

const x = { ...fuga, ...hoge1 };
const y = { ...fuga, ...hoge2 };

このようにbarを必須プロパティにすると、x.bary.barnumber型と推論されます。しかし、y.barundefinedです。型と実際の値が食い違ってしまいます。TypeScriptの型を信じて安心していたらJavaScriptに背後から刺された気分です。

追記) 
コメントでberlysiaさんから、
exactOptionalPropertyTypesオプションを使えば、hoge2を検出できると教えていただきました。

いかがでしたか?オブジェクトのプロパティに明示的にundefinedを指定するときは注意しましょう。その変数、本当に正しく型づけされていますか?

Discussion

berlysiaberlysia
mm1995tkmm1995tk

ありがとうございます!!
後ほど追記させていただきます🙇‍♂️

そもそも明示的にundefinedを書くのがあまりよろしくないんですかね…

petamorikenpetamoriken

この挙動はオブジェクトが引数で指定したプロパティを持つか確認する.hasOwnPropertyメソッドを実行してみると謎が解けます。

hoge1.hasOwnProperty('bar') // false
hoge2.hasOwnProperty('bar') // true

今ですと ES2022 Object.hasOwn を使うほうが推奨されていますね。Safari が 15.4 からしか使えないので、対応ブラウザを考慮する必要はありますが……。

Object.hasOwnProperty() よりも推奨される理由は、Object.create(null) を使って作成したオブジェクトや、継承した hasOwnProperty() メソッドをオーバーライドしたオブジェクトに対して動作することです。これらの問題は、外部オブジェクトの Object.prototype.hasOwnProperty() を呼び出すことで回避できますが、Object.hasOwn() の方がより直感的に理解しやすいでしょう。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn

Haru_YakumoHaru_Yakumo

undefined 渡されたときに key ごと消したい場合は Object.entries を使ってキーと値で分離して filter かけたものを Object.fromEntries に食わせると消せますね

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/entries

Object.fromEntries(
  Object.entries(hoge).filter(([, val]) => typeof val !== "undefined")
);
mm1995tkmm1995tk

現状私もこれをやってますが、ランタイムに型チェックしてるようなものだし静的型付けとは…みたいな気分になってもやもやします🥲