🍭

JavaScript で undefined の使用が推奨されない理由

2020/11/27に公開
4

はじめに

この記事は #EveOneZenn (Everyday One Zenn) vol.05 です。

JavaScript で undefined を値として使用することが推奨されない理由についてまとめます。

(過去に別所で公開していた記事の加筆版です)

前回:
https://zenn.dev/lollipop_onl/articles/eoz-ts-no-unchecked-indexed-access

TypeScript Deep Dive の見解

TypeScript Deep Dive という TypeScript のバイブル的リファレンスに次のようなページがあります。

https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

このページでは undefined を値として使用するのを推奨していません。

たとえば、値が undefined かどうかを調べるには null との比較を推奨していたり、

// 👎 Not good...
obj.prop === undefined;
 
// 👍 Better
obj.prop == null;

ルートレベルの変数が定義されているかを判断するには typeof の使用を推奨していたりします。

// 👎 Not good...
globalObj === undefined;
 
// 👍 Better
typeof globalObj === 'undefined';

ただ、TypeSCript Deep Dive には undefined を使用するべきではない具体的な理由については言及されていません。

ESLintルールの見解

同様の主張をしている ESLint の no-undefined ルールのドキュメントで具体的な理由が言及されていました。

https://eslint.org/docs/rules/no-undefined

説明の一部を抜粋します。

undefined は代入可能なグローバルオブジェクトのプロパティです。 ECMAScript 3 では undefined を上書きすることができます。 ECMAScript 5 からグローバルの undefined を上書きできなくなりましたが(代入はできる)、スコープ変数として undefined をシャドーウィングすることができます。

つまり、 undefined 自体は代入可能な変数のため、実装に寄っては undefined に別の値が代入されることがあり得るということです。

次のコードは ES3 時代にグローバル変数として存在する undefined に異なる値を代入する例です。
ES5 以降ではグローバル変数の undefined への代入でエラーは発生しませんが、代入が反映されません。

// ES3
window.undefined = 1;
// undefined === 1

const obj = {};

obj.prop === undefined; // false

// ES5
window.undefined = 1;
// undefined === undefined

また、次のコードは現在でも再現できる、スコープ変数として undefined を定義する例です。

const foo = () => {
  const undefined = 1;
  // undefined === 1

  const obj = {};

  obj.prop === undefined; // false
};

no-undefined ルールでは undefined の使用に関するガイドラインを次のように提示しています。

  • undefined は初期化されていない変数にのみ使用する
  • 値が undefined かどうかの判定は typeof 演算子を使用する

なお、 no-undefined ルールは recommendedeslint-config-airbnbeslint-config-standardeslint-config-google のいずれのルールセットでも有効化されていません。

undefined を保護する

代入やシャドーウィングから undefined を保護するには、ESLint の no-global-assign ルールと no-shadow-restricted-names を使用します。

https://eslint.org/docs/rules/no-global-assign

https://eslint.org/docs/rules/no-shadow-restricted-names

おわりに

余談として、 null はプロパティ以外では代入も宣言もできない値なので、 undefined のような問題は発生しません。

const null = 1;
// SyntaxError: Unexpected token 'null'

const foo = () => {
  const null = 1;
  // SyntaxError: Unexpected token 'null'
};

window.null = 1;
// null === null
// window.null === 1

参考

GitHubで編集を提案

Discussion

standard softwarestandard software

undefinedの使用を推奨しないのは値が書き換得られる可能性があるから、ということではないと思います。

nullは明示的に空を表す値なので、何かを空に設定するならnullを使うべきで
undefinedは、値がない、ということを示すので、設定するときに自分のコードで代入する
ということは、あまりないようにするべきということになります。

DeepDriveのコードですが下記の通りです。

BAD
function foo(){
  // if 何らかの場合に返す値
  return {a:1,b:2};
  // else それ以外の場合に返す値
  return {a:1,b:undefined};
}

GOOD
function foo():{a:number,b?:number}{
  // if 何らかの場合に返す値
  return {a:1,b:2};
  // else それ以外の場合に返す値
  return {a:1};
}

b に undefined を入れるなら、bの値が存在しないように記載しようということです。

ライブラリを作っていると undefined を使わないと行けない場面は多々あるのですが
ライブラリ使用者がundefinedを代入して使うということは少なくなるように作るべきと思います。

また、TypeScriptチームはnullを使わないというのは賢明な判断だと思いますが、

TypeScript Deep Drive で書かれている、==nullで判断するというのは実に危ないコードになります。
False属性は、falseと判定するのがたくさんの値になってしまっているので、コードがよみにくく不具合の温床になってしまうのですが、同様に、 null と undefined の同一化してコードを書くと結局は不具合の温床になります。

1と1.001 がプログラム上、別の値と同じように、null と undefined も似てはいますが別の値なので、1文字のミスも許されないプログラミングという行為において、別の値を同一視してコードを書くのは、後々やばい問題を引き起こすことが多いです。

TypeScript Deep Drive が null と undefined を同一視を推奨しているのが、とても謎ですが、この1点で、私は上級者が参考にするような情報ではないと感じてます。

例えば、下記のコード。

if (a == null) {
  testFunc(a);
}

const testFunc = (value === 'Hello') => {
  console.log(value);
}

a が null の場合と、undefined の場合、testFuncとconsole.logが呼び出されますが、どちらでもHelloと出力されるわけではないからです。

JS と TS では、常に「==」は悪で「===」が善なので、TypeScript Deep Drive で示している思想は不具合を起こしかねない書き方になります。

𝕤𝕚𝕞𝕠𝕔𝕙𝕖𝕖𝕤𝕚𝕞𝕠𝕔𝕙𝕖𝕖

コメントありがとうございます!

undefined は言葉の通り「定義されていない」を表すものなので、値として使用するのは避けるべき、というのはおっしゃるとおりかと思います。
また、 == null は JavaScript の複雑な Falsy を利用しているため、わかりづらいというのも理解します。

ただ、 == null については ESLint の eqeqeq ルールにも例外的に == null を除外するオプションがあるように、 == null で Nullable を判定するのはメジャーな方法なのかなと思っています。

もちろん、厳密に undefined かを判定するには typeof foo === 'undefined' を使うべきだとは思いますが、一般的なアプリケーションでは == null を使用して問題が発生するケースは少ないのではないか、というのが私の意見です。

standard softwarestandard software

ESLintは様々なニーズに対応できるように「==」の中で「==null」だけ許容するというオプションがあるのは納得できます。「==null」は「null」と「undefined」だけを見分けるのはそのとおりなのですが、上記で記載したようにnullとundefinedを区別しておかないとプログラムの挙動が変わるものがあるので、区別せざるおえないです。変数にnullかundefined、どっちかが入っているかわからない、なら、どっちに処理がいくのかわからない、なら、結局動くのか動かないのかわからない。となり、書いている時は平気なのですが、後々技術的負債につながっていきます。isNullとisUndefinedとisNullOrUndefined(==null)は明確に区別したほうがおすすめです。

区分けしないコードの中でnullかundefinedかで不具合を起こす危険性を排除できないのでコード組むのがかなり難しくなるので、初心者や中級者は安易に使ってしまうのですが上級者がそれだと品質の高いコードに遠のいてしまう、という感じです。

𝕤𝕚𝕞𝕠𝕔𝕙𝕖𝕖𝕤𝕚𝕞𝕠𝕔𝕙𝕖𝕖

ありがとうございます。

「上級者であれば厳密に判定したほうがコードの質が向上する」というご意見には同意します。
nullundefined は値として異なるものとして存在するし、実装上でも異なるものとして扱うべきというのもごもっともです。

変数にnullかundefined、どっちかが入っているかわからない、なら、どっちに処理がいくのかわからない、なら、結局動くのか動かないのかわからない。となり、書いている時は平気なのですが、後々技術的負債につながっていきます
最近 TypeScript 上でしか JavaScript に触れていなかったため、この意見は思いつきませんでした。
型という概念が一切存在しない JavaScript そのものにおいては確かに、nullundefined を区別しておいたほうが混乱しなくてよさそうですね。