1
1

はじめに

いきなりですが、以下のコードを読んで違和感を感じませんか?
ちょっとでも「ん??😕」と思った方はこちらの記事を読んでいただけると嬉しいです🎉

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と定義されていました。

node_modules/socket.io-client/build/esm/socket.d.ts
    /**
     * 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の場合はエラーになるが正しそうです。
こちらのコード、strictNullChecksfalseの場合はエラーは出ません。(個人的にはとても気持ち悪い😥)

strictNullChecksがfalseの場合
const example: string = undefined; // OK
strictNullChecksがtrueの場合
const example: string = undefined; // NG
// error TS2322: Type 'undefined' is not assignable to type 'string'.

const example: string | undefined = undefined // OK

普段の実装では、strictNullCheckstrueなのでundefinedを取りうるのに型定義にundefinedが含まれないことにもとても違和感を感じました。

終わりに

undefinedの扱い、いまだによく分かっていない気がします…難しい🥺
簡単な例ですが、違和感を持ったので調べてみたら発見がありました💡

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1