😇

nullの扱いに注意...(TSでの工夫)

に公開5

こんにちは。
てるし〜です。

JavascriptやTypescriptでプログラムを書いているとnullが出てくると思います。

みなさんはこれらを使っていますか?

nullを使う場合はしっかりとハンドリングする必要があります。

ですが、経験の浅い私はうっかり雑なハンドリングをしてしまいバグを起こしてしまったという記憶がありました。

経験豊富な方(上級者)からしたら「こんなことしなくても良いだろ!」といった意見はあるかと思いますが、こんなやり方もあるよということで記事にしました。

賛否両論はあると思いますが、個人的にプロダクトに導入して良かったと思うものではあるので参考にしてください。

nullとは

nullは何も値がない場合、または値が不明な場合を表すものです。
このnullはJavascriptにも実装されています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/null

Javascriptに実装されているということはTypescriptにも実装されているわけです。

nullは10億ドルの損失ともいわれた!?

nullを作成した開発者Tony Hoareは、"Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い)と言っています。

続きの説明を引用すると

私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。

https://doc.rust-jp.rs/book-ja/ch06-01-defining-an-enum.html#option-enumとnull値に勝る利点

nullの実装はとても簡単なのでTonyはそれの誘惑に負けてしまったと言います。結果、システムのクラッシュや脆弱性が見つかり10億ドルの損害を起こしたであろうという意味で上記の言葉を発しました。

nullはコンパイラが認識することはとても厳しいものであり、Javaではよく「ヌルポ」と呼ばれるNullPointerException、Javascript/TypescriptでもTypeErrorが起こります。

これらのエラーは実行時に起こるので実質バグという形になります。

nullを使った時の勘違いのミス

一例を見ていきましょう。

import { useEffect, useState } from "react";

function App() {
  const [num, setNum] = useState<number | null>(null);

  useEffect(() => {
    let ignore:boolean = false;
    const random:number = Math.floor(Math.random() * 10);

    setTimeout(() => {
      if (ignore) {
        console.log(random);

        setNum(random);
      }
    }, 2000);

    return () => {
      ignore = true;
    };
  }, []);

  return (
    <>
      <div>
        {num ? num : "取得中"}
      </div>
    </>
  );
}

export default App;

上記はReactのソースです。
簡単に説明すると、

レンダーされて2秒後に0~10の整数を表示するというものです

わかる人にはわかるとは思います。
これが疲れていたり言語仕様がわかっていないと一見何も問題ないように見えます。
ですが、1つ問題があるのです。

上記画像の通り、「取得中」という文字が出たままになってしまっています。

ログを見てみると0が返ってきています。

JS/TSでは0はfalseに該当するので上記のような結果になります。

他にも文字列なら''がfalseに該当します。
さらに、nullbooleanとなった場合も混乱を起こすこともあるでしょう。

上記コードを修正するなら

return (
    <>
      <div>
        {num!==null ? num : "取得中"}
      </div>
    </>
  );

とするのが良いかと思います。

ですが、nullは上記のように混乱を招く可能性が大です。
ってなった時になるべく混乱しないようにしたいですよね。

後日追記
コメントに上記修正方法の提案をいただいたので記載します。
ありがとうございます🙇🏻‍♂️

  1. numberへの統一
const [num, setNum] = useState<number>(-1);
.
.
.
return (
    <>
      <div>
        {num > 0 ? num : "取得中"}
      </div>
    </>
  );
  1. eslintの活用

Option

RustではOptionをnullの代わりに用いています。
コンパイラが型で致命的なバグを防ぐために認識できる形にするためです。
そしてこのOptionは必ずmatchif letで剥がさないと怒られる仕様になっています。

今回はRustの記事はでないので詳細は語りませんが、気になる方はRustのドキュメントを読んでください。

TSでOption

私の頭の中にはOptionはTSの中に存在していないという認識です。
なら自分で作ってしまえ!といった感じです。

型定義

ではどのように定義するのでしょうか?

export const OPTION_SOME = "some" as const;
export const OPTION_NONE = "none" as const;

interface Some<T> {
  readonly kind: typeof OPTION_SOME;
  value: T;
}

interface None {
  readonly kind: typeof OPTION_NONE;
}

export type Option<T> = Some<T> | None;

と上記のように定義します。

ここでNG例を出しておきます

export interface NgOption<T> {
  readonly kind: typeof OPTION_SOME | typeof OPTION_NONE;
  value?: T;
}

です。

これをやってしまうと

  • noneの時でも値を入れることができてしまう
  • someの時でもvalueを未定義にできてしまう
    といった問題が起こり型定義している意味がなくなってしまいます。

Optionを生成するオブジェクトを用意

これは作っておくと便利なので紹介しておきます。

export const createOption = {
  some: <T>(value: T): Some<T> => {
    return {
      kind: OPTION_SOME,
      value,
    };
  },
  none: (): None => {
    return {
      kind: OPTION_NONE,
    };
  },
};

上記コードを作成しておけば

createOption.some<number>(1)
/**
 *{
 *    kind:"some",
 *    value:1,
 *}
 */
createOption.none()
/**
 *{
 *    kind:"none",
 *}
 */

というように簡単なOption Objectが出来上がります。

nullOptionにチェンジ

では、サンプルを書き換えていきます。

- const [num, setNum] = useState<number | null>(null);
+ const [num, setNum] = useState<Option<number>>(createOption.none());

  useEffect(() => {
    let ignore:boolean = false;
    const random:number = Math.floor(Math.random() * 10);

    setTimeout(() => {
      if (ignore) {
        console.log(random);

-       setNum(random);
+       setNum(createOption.some<number>(random));
      }
    }, 2000);

    return () => {
      ignore = true;
    };
  }, []);

こうするとなんと!!!!!!

はい、おかしいとエラーが吐かれました!!!

ではこのエラーを解消していきます。

  return (
    <>
-      <div>{num ? num : "取得中"}</div>
+      <div>{num.kind === OPTION_SOME ? num.value : "取得中"}</div>
    </>

これでエラーが解消され、それと同時に0が入っても0が表示されるようになりました!!!!

修正後のコードを下記に記載しておきます。

import { useEffect, useState } from "react";
import { createOption, OPTION_SOME, type Option } from "./utils/option";

function App() {
  const [num, setNum] = useState<Option<number>>(createOption.none());

  useEffect(() => {
    let ignore = false;
    const random = Math.floor(Math.random() * 10);

    setTimeout(() => {
      if (ignore) {
        console.log(random);
        setNum(createOption.some<number>(random));
      }
    }, 2000);

    return () => {
      ignore = true;
    };
  }, []);

  return (
    <>
      <div>{num.kind === OPTION_SOME ? num.value : "取得中"}</div>
    </>
  );
}

export default App;

まとめ

今回、nullの扱いについて解決方法の例を提示しました。
nullの扱いに慣れている人であればなんちゃないことではありますが、私の考えではハンドリングができていてもソースコードの可読性が落ちるであったり上記ミスの原因に気が付かず時間を使ってしまうなどの問題が挙げられます。

eslintを使ったり型でガードといった手法、そして私が提示したOptionを使うなどしてチャチなミスを減らすというのも一つテクニックとして挙げられるのではないかと考えています。

Discussion

dameyodamedamedameyodamedame

そもそもnullが問題になっている実例はこの記事内にはない気がします。
Boolean(0)がfalseなだけですよね。
nullの風評被害な気がします。

nullを使ったが故に間違ったのではなく、number | nullと明記しているのに、雑な判定をしただけですよね。懸念だけで色々するくらいなら、実例をきちんと調べて記事にしましょう。

元記事は恐らく以下ですが、

https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/

ここにも具体的な事例があるわけではありません。

...
const [num, setNum] = useState<number>(-1);
...
{num < 0 ? "取得中" : num}

number型に統一して境界値で分けても大丈夫です。

この話だけでは型で分けた上で暗黙のキャストをなくすことにより、手堅く意図を明確に出来てより良いという程度で、そうでないと悪いというほどの話ではないように思います。

私がGemini2.5 flashに事例を聞いたところ、URLまで持ち出せるような直接的な報道は非常に稀で、セキュリティ関連しか出てこないと言ってましたね。

https://www.openssl.org/news/secadv/20210325.txt
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-0263

個人的な感覚では、確かにnullやNULLやnullptrにはよく泣かされますが、大半は意図しないようなタイミングで不用意にnullが入り、クラッシュするような不具合です。原因となるバグ自体は遠い場所にある些細なミスで、それが無関係なくらい遠い先でクラッシュを起こします。さほど使わないからかもしれませんが、JavaScript/TypeScriptで泣かされたことはありません。

てるし〜てるし〜

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

ご指摘いただいてとても感謝です。
Rustのドキュメントを読んでいて「あ〜そうなのか〜」という勢いそのもので書いてしまったので反省しております。

別の定義方法から資料までありがとうございます。

とても、参考になるコメントでしたので今後に活かしていきます🙇🏻‍♂️

1つ質問させてください。
これが例えば全領域(マイナスを含む)の整数値であると上のような定義はできなくなると思いますが、そのような場合はdameyodamedameさんならどう定義されますか?

dameyodamedamedameyodamedame

1つ質問させてください。
これが例えば全領域(マイナスを含む)の整数値であると上のような定義はできなくなると思いますが、> そのような場合はdameyodamedameさんならどう定義されますか?

  • Result/Option系の実装
  • オブジェクトにしてタイプ判定用のメンバ追加
  • null使用

のいずれか。多分楽なのでこの程度ならnullにします。他で使わないので。

ぬまさんぬまさん

numberstring!= null でnullチェックしないと0や空文字もfalsyなので期待しない条件で処理しちゃう心配がありますよね。記事を少し読みましたが要は != null などで厳密に条件チェックをちゃんとしましょうということであれば、例えば strict-boolean-expressionsというESLintルールを導入して静的チェックするアプローチもありなのかなと思いました。

StackBlitzで検証コードを書いてみましたが、スクショのようにESLintでキチンと検知できていました。こちらのコードをreturn num != null ? num : '未定義' のようにnullチェックの書き方に変えるとlintエラーは無くなります。

てるし〜てるし〜

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

linterでのガードは全く考えにありませんでした!と同時にそのようなESLintルールもあったのですね。
勉強になります。

知らないことが知れるきっかけになるのでとても感謝しています🙇🏻‍♂️