はじめに
いきなりですが、以下のコードを読んで違和感を感じませんか?
ちょっとでも「ん??😕」と思った方はこちらの記事を読んでいただけると嬉しいです🎉
const text: string = 'example'
if (text === undefined){
return
}
厳密等価演算子を使ったstring型とundefind型の比較で、コンパイルは通ったのですが違和感がありました。
簡単な例ですが、発見があったのでまとめます。
先に結論を書きます。
JavaScript(TypeScript)では、どんな型の変数も undefined と比較することができる
undefined === undefined // true
undefined === null // false
undefined === 0 // false
undefined === "" // false
undefined === true // false
背景
WebSocketサーバの実装で導入しているライブラリの型定義で誤りがあることに気がつきました。
ソースコードにコメントで記載されていますが、socketインスタンスはidプロパティを持ちます。このidプロパティはWebSocketのコネクションが開通するまではundefindであり、開通後は文字列が割り当てられます。
よって、型定義はid: string | undefind
が正しいはずです。
実際にライブラリの最新バージョン(v4.7.5)では、id: string | undefind
と定義されています。
しかし、プロダクトに導入されていたv4.5.4では以下のようにid: string
と定義されていました。
/**
* A unique identifier for the session.
*
* @example
* const socket = io();
*
* console.log(socket.id); // undefined
*
* socket.on("connect", () => {
* console.log(socket.id); // "G5p5..."
* });
*/
id: string;
/**
要件として、コネクションが開通したら実行したい処理があったのでこのidプロパティを使って条件分岐することを検討しました。
型の誤りを見つけましたが、①実装上は問題なさそうなこと、②一気にライブラリのバージョンを上げるのはリスクがあることから、今回はこのままの型定義で条件分岐を実装することになりました。
実装
idプロパティはWebSocketのコネクションが開通するまではundefind
であり、開通後は文字列が割り当てられます。
この仕様をもとに、以下のような条件分岐を考えました。
// コネクションが開通しているか確認
if (socket?.id === undefined) {
return
}
// ...実行したい処理
実装自体はこれで問題なさそうです🎉
ですが、ここでものすごい違和感を感じました。
id: string
と定義されているのに、なぜundefiend
と比較してエラーが起きないの???
例えば以下のように数字の1
と比較した場合、当然コンパイルエラーが発生します。
// 'string' 型と 'number' 型が重複していないため、この比較は意図したとおりに表示されない可能性があります。
if (socket?.id === 1) {
return
}
気になってしまったので調べてみました。
調査
string型とnumber型を比較したときエラーになる理由
まずはsocket?.id === 1
がエラーになる理由を考えます。
socket.id
がstring型として宣言されているのに対し、1
はnumber型です。
TypeScriptは異なる型同士の比較を許可しないため、コンパイル時にエラーとなります。
これは直感に合っていますよね。
string型とundefinedを比較したときコンパイルを通る理由
ではsocket?.id === undefined
がエラーにならず、コンパイルを通ってしまう理由を考えます。
JavaScript(TypeScript)では、どんな型の変数も undefined と比較することができます。
ほんとか????やってみた。
undefined === undefined // true
undefined === null // false
undefined === 0 // false
undefined === "" // false
undefined === true // false
まじでエラーでないやん。
根拠となるようなドキュメントは探せなかったのですが、動作を見る限りは本当みたいです。
この仕様であれば、当然socket?.id === undefined
のようにstring型とundefinedを比較したときにエラーは出ません。
自分なりに解釈してみる
仕様なので突っ込んでも仕方ないのですが、理由を生成AIで調査してみました。
TypeScriptの型システムでは、undefined はほとんどの型と互換性があります。これは、変数が初期化されていない状態や、オプショナルな値を表現するためです。
要は、「JavaScriptでは値が代入されていない場合もundefinedだから、TypeScriptでnullチェックとかするときにundefinedと比較するのを許可しなかったらみんな困るよな??」 的なニュアンスかなと解釈しました。
ここら辺、もうちょっと深く知ってみたい…
余談
ライブラリのidの型定義が間違っていたと記載しましたが、実際はstrictNullChecks
がtrueの場合はエラーになるが正しそうです。
こちらのコード、strictNullChecks
がfalse
の場合はエラーは出ません。(個人的にはとても気持ち悪い😥)
const example: string = undefined; // OK
const example: string = undefined; // NG
// error TS2322: Type 'undefined' is not assignable to type 'string'.
const example: string | undefined = undefined // OK
普段の実装では、strictNullChecks
はtrue
なのでundefined
を取りうるのに型定義にundefined
が含まれないことにもとても違和感を感じました。
終わりに
undefined
の扱い、いまだによく分かっていない気がします…難しい🥺
簡単な例ですが、違和感を持ったので調べてみたら発見がありました💡