等価演算子とか厳密等価演算子とかnullとかundefinedとか
2021-08-05

等価演算子を噛み砕く

この記事ではJavaScriptはjs, TypeScriptはtsと長く書くのめんどいから呼称するよ。

jsでの比較で迷うものの一つに、 == と === がある。 jsはエラーが起きにくい仕様(よしなにやってくれる)であるため、なんとなくでも使えてしまう反面、エラーになったときに原因を探ることが難しくなります。

== は2つの変数の型変換を行います。 例えば、数字の3と文字列としての3は、==での比較ではtrueになります。比較対象が異なる型の場合、型をできるだけあわせてから比較を行います。

ちなみに、”できるだけ”という表現は厳密には適切では有りません。2つのデータ型によって、どのように変換が行われ、比較されるかが決まります。

例えば、数値と論理値の比較を行う際は、論理値はそれぞれ1,0に変換され、比較が行われます。文字と論理値であればそれぞれ数値型に変換されてから比較が行われます。

なので、jsでは、

'1' == true

が真となります。これはしばしば、他の言語に慣れている人間が落とし穴にハマります。0や空文字は jsにおける比較ではfalslyな値として扱われるためです。

  if (0) {
    console.log('zero')
  }else{
    console.log('zero is falsly')
  }
  // => zero is falsly

厳密等価演算子と呼ばれる、=== に関しては、==で行われるような暗黙の型変換は行われません。なので、==でtrueになるような比較はすべてfalseになります。

1 === '1' // false
1 === 'true' //false

オブジェクトに関してはもう少し複雑です。

const a = {a: 1}
const b = {a: 1}
const c = a
a == b // false
a == c // true
c == b //false

同様の形をしていても、a == b はfalseの判定になります。一方、cはaを参照しているので、比較を行った場合はtrueになります。比較は、参照に対して行われます。

基本的には現在の通常の開発で変更が参照にも及ぶことや、比較が参照に対して行われることはあまり意識する必要はなくなってきていますが、varはともかくとしてletは使用するケースが有るので頭の片隅には置いておくと時間を無駄にせずに済むかもしれません。

オブジェクトごと比較を行いたい場合は、 deep-equalというパッケージがあり、それを利用することでオブジェクトをまるごと比較対象にすることができますが、殆どの場合でそれが必要になるケースは有りません。

比較を行いたい場合は、オブジェクトの中身をピックして比較しましょう。idなどの特定可能なプロパティを用いるのが良いです。

基本的に == のふわっとした挙動を許可することにメリットはないので、tsでも===を使っていきましょう。

例外として、nullとundefindに関しては別途考慮する必要があります。

js(tsも)では,null, undefinedという初心者を惑わせる2つの型が存在します。この2つは異なる意味を持っています。( 記事時点で筆者はundefindは初期化されてない、nullは明示的に”何もない”を表現してる、区別して認識していますが、利用云々、使うべきかどうかという点などについてはここでは割愛します。)比較演算子に比較させる場合には、ややこしい挙動を行うため、混乱のもとになります。

https://github.com/sindresorhus/meta/discussions/7

null undefinedの両者とも、とりあえずfalslyな値だな、位の感覚でいる位のレベルの人間が一番厄介です。(私です) if分岐でfalse判定になるからといっても、下記の条件ではfalslyな値としてtrue判定にはなりません。

0 == null // false
false == undefined //false
'' == null //false

単品で見れば、そりゃそうだろ、という感覚になりますよね?何が言いたいのかというと、単にflaslyな値としてひとくくりにすると、痛い目を見る可能性があるということです。

nullとundefinedに話を戻しますが、この2つは比較した場合に下記のような結果になります。

null == null //true
undefined == undefined // true

上記は当然ですがtrueになります。一方で、

null == undefined //true

もtrueになります。これをうまく利用すると、実務で必要なnullとundefinedをいい感じにチェックすることができます。 よくあるのは、関数の引数などで、nullやundefinedを排除したい場合です。

const hoge = (val: number | null | undefined )  => {
    //nullやundefinedじゃないときだけ何か処理をしたい
}

こういうケースでは、上記特性を活かすことで両方チェックできます。

const hoge = (val: number | null | undefined )  => {
    if (val != null) {
        //この中のvalは確定でnumber
    }
}

自身はif文でfalslyなのでそのような場合は早期リターンをよく使うのですが、覚えておくと人のコードを読んだりするときに捗ります。

ちなみに、じゃあ !== とか、 === とかはどうなるの?という話ですが、nullundefinedは、型としては別物なので、

const hoge = (val: number | null | undefined )  => {
    if (val !== null) {
        //この中のvalはnullではないがundefindの可能性がある
    }
}

ということになり、undefindを弾くためには冗長なコードを強いられることになります。

おまけ

Nullish Coalescing

??です。 上述したように、js(ts)は、0や空文字をfalslyな値として評価します。とはいえ、変数の結果として0という値を利用したいケースや、falseとnull,undefindを区別して処理を行いたいという場合も出てきます。そのような場合は、この比較演算子が便利です。

hoge ?? 'foo'

上記の例では、hogeがnullか、undefindであった場合にのみ、fooを返します。これは、

hoge != null ? hoge : 'foo'

これと結果が同じになります。3.7以降のtsで使用できるので、使える環境ならば積極的に使っていくとちょっとかっこいい感じになります。

オプショナル型

ちょっと話は変わるが、tsにはオプショナル型というものがある。 インターフェースや、関数の引数などで、プロパティの末尾に?をつけることによって、オプショナルプロパティ、オプショナルパラメータとして扱うことができます。

interface Hoge {
    foo: string
    bar?: number //これ
}

これをvscodeなんかでカーソルを合わせてみると、こんな内容のポップアップが出てくる。

(property) Hoge.bar?: number | undefined

「Hogeのbarプロパティは、numberか、あるいはundefindである」と言っているように見受けられますね。

ここで疑問が浮かびませんか?、「自分でこの形定義出来るくね?」と。

interface Fuga {
    foo: string
    bar: number | undefined //これ
}

これにカーソルを合わせると

(property) Fuga.bar: number | undefined

となります。”ほぼ”一緒ですね?barの末尾に?がついていることを除けば(Hoge,Fugaは区別のためです。)、barがnumberか、undefinedのどちらかであるという表現としては同一のものに見えます。

この2つの違いは、そのプロパティ自体を省略できるかどうか(例外有り、クラスのコンストラクタ)です。 Hogeの方は、barを省略してもよいので、

const hoge:Hoge = {foo: 'foooo!' } 

としても、エラーになりません。しかし、Fugaの方はエラーになってしまいます。

const fuga: Fuga = { foo: 'foo'} //コンパイルエラー

これに関してはどっちをどう使い分けるべきなのか自分の中で定まっていません。教えてほしい。 関数の戻り値としてなら型アノテーションで表現したほうが良さげな感じはあるが、propsとして渡したり、関数のプロパティパラメータとして渡すときとかどっちがいいんだろうか。

JSONとundefined

JSONは、標準ではundefinedに対してエンコードのサポートをしてない。値としてnullなものは、エンコード時に含まれるが、undefinedなものは除外される。