nullの扱いに注意...(TSでの工夫)
こんにちは。
てるし〜です。
JavascriptやTypescriptでプログラムを書いているとnull
が出てくると思います。
みなさんはこれらを使っていますか?
nullを使う場合はしっかりとハンドリングする必要があります。
ですが、経験の浅い私はうっかり雑なハンドリングをしてしまいバグを起こしてしまったという記憶がありました。
経験豊富な方(上級者)からしたら「こんなことしなくても良いだろ!」といった意見はあるかと思いますが、こんなやり方もあるよということで記事にしました。
賛否両論はあると思いますが、個人的にプロダクトに導入して良かったと思うものではあるので参考にしてください。
null
とは
null
は何も値がない場合、または値が不明な場合を表すものです。
このnull
はJavascriptにも実装されています。
Javascriptに実装されているということはTypescriptにも実装されているわけです。
null
は10億ドルの損失ともいわれた!?
null
を作成した開発者Tony Hoareは、"Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い)と言っています。
続きの説明を引用すると
私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。
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に該当します。
さらに、null
とboolean
となった場合も混乱を起こすこともあるでしょう。
上記コードを修正するなら
return (
<>
<div>
{num!==null ? num : "取得中"}
</div>
</>
);
とするのが良いかと思います。
ですが、null
は上記のように混乱を招く可能性が大です。
ってなった時になるべく混乱しないようにしたいですよね。
後日追記
コメントに上記修正方法の提案をいただいたので記載します。
ありがとうございます🙇🏻♂️
-
number
への統一
const [num, setNum] = useState<number>(-1);
.
.
.
return (
<>
<div>
{num > 0 ? num : "取得中"}
</div>
</>
);
- eslintの活用
Option
型
RustではOption
をnullの代わりに用いています。
コンパイラが型で致命的なバグを防ぐために認識できる形にするためです。
そしてこのOption
は必ずmatch
やif let
で剥がさないと怒られる仕様になっています。
今回はRustの記事はでないので詳細は語りませんが、気になる方はRustのドキュメントを読んでください。
Option
型
TSで私の頭の中には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が出来上がります。
null
をOption
にチェンジ
では、サンプルを書き換えていきます。
- 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
そもそもnullが問題になっている実例はこの記事内にはない気がします。
Boolean(0)がfalseなだけですよね。
nullの風評被害な気がします。
nullを使ったが故に間違ったのではなく、number | nullと明記しているのに、雑な判定をしただけですよね。懸念だけで色々するくらいなら、実例をきちんと調べて記事にしましょう。
元記事は恐らく以下ですが、
ここにも具体的な事例があるわけではありません。
number型に統一して境界値で分けても大丈夫です。
この話だけでは型で分けた上で暗黙のキャストをなくすことにより、手堅く意図を明確に出来てより良いという程度で、そうでないと悪いというほどの話ではないように思います。
私がGemini2.5 flashに事例を聞いたところ、URLまで持ち出せるような直接的な報道は非常に稀で、セキュリティ関連しか出てこないと言ってましたね。
個人的な感覚では、確かにnullやNULLやnullptrにはよく泣かされますが、大半は意図しないようなタイミングで不用意にnullが入り、クラッシュするような不具合です。原因となるバグ自体は遠い場所にある些細なミスで、それが無関係なくらい遠い先でクラッシュを起こします。さほど使わないからかもしれませんが、JavaScript/TypeScriptで泣かされたことはありません。
コメントありがとうございます!
ご指摘いただいてとても感謝です。
Rustのドキュメントを読んでいて「あ〜そうなのか〜」という勢いそのもので書いてしまったので反省しております。
別の定義方法から資料までありがとうございます。
とても、参考になるコメントでしたので今後に活かしていきます🙇🏻♂️
1つ質問させてください。
これが例えば全領域(マイナスを含む)の整数値であると上のような定義はできなくなると思いますが、そのような場合はdameyodamedameさんならどう定義されますか?
のいずれか。多分楽なのでこの程度ならnullにします。他で使わないので。
number
やstring
は!= null
でnullチェックしないと0や空文字もfalsyなので期待しない条件で処理しちゃう心配がありますよね。記事を少し読みましたが要は!= null
などで厳密に条件チェックをちゃんとしましょうということであれば、例えばstrict-boolean-expressions
というESLintルールを導入して静的チェックするアプローチもありなのかなと思いました。StackBlitzで検証コードを書いてみましたが、スクショのようにESLintでキチンと検知できていました。こちらのコードを
return num != null ? num : '未定義'
のようにnullチェックの書き方に変えるとlintエラーは無くなります。コメントありがとうございます!
linterでのガードは全く考えにありませんでした!と同時にそのようなESLintルールもあったのですね。
勉強になります。
知らないことが知れるきっかけになるのでとても感謝しています🙇🏻♂️