🥶

【JS/TS】Javascript/Typescriptで`null`や`undefined`撲滅!?

2025/02/08に公開

はじめに

どもどもてるし〜です。

最近Rustを勉強し始めました。
今までや意識してなかったことや新しい知識を取得できとても有意義な時間を過ごしています。

Rustのドキュメントを日本語訳したものを現在読んでいます。

https://doc.rust-jp.rs/book-ja/

enumの単元を読んでいるときに以下の言葉が出てきました。

"Null References: The Billion Dollar Mistake"

私はこの言葉にすごく興味を持ちました。

今回はこの言葉はいったいどういう意味をなのか?
そして、JSやTSにどう活かしていく?
といった記事になります。

"Null References: The Billion Dollar Mistake"とは

nullを開発したことでバグが多く出てしまい大きな損害を与えてしまったことで上記の言葉を開発者が発しました。

より詳細に関しては日本語での記事がなかったのでChatGPTに聞いてしまいました。

背景

nullは1995年Tony Hoareが ALGOL W を設計するのに導入しました。
ですが、nullによってバグやシステム障害が発生し大きなコストを払ってきました

nullによる問題

問題は主に以下の通りです。

  1. プログラムがクラッシュする
  2. 見落としやすいバグが発生する
  3. 取り扱いミスがセキュリティの脆弱性に繋がる

これらはシステムを運用する上で致命的なものになります。
詳細はここでは省略します。

null問題を防ぐためには??

  1. TypeScriptのstrictNullChecks
  2. Option型やResult型を作成する(最近の言語ではこれでnullを禁止している)
  3. nullを使わずデフォルト値を設定する

です。

実際にJS/TSのコードで実践してみる

JSやTSでもnullundefinedは出てきます。
1つのソースコードを例にnullundefinedを使わない方向性で安全なソースコード🧐🧐にしてみようと思います。

対象のソースコード

以下のソースコードを対象とします。

popup-context.tsx
"use client"

import { createContext, ReactNode, useContext, useReducer } from "react"
import { popupContextType } from "./popup.types"
import { popupInitialState } from "./popup-initial-state"
import { popupReducer } from "./popup-reducer"
import PopupLayout from "@/layout/popup-layout"

export const PopupContext = createContext<popupContextType | undefined>(
    undefined
)

interface Props {
    children: ReactNode
}

export function PopupProvider({ children }: Props) {
    const [state, dispatch] = useReducer(popupReducer, popupInitialState)

    return (
        <PopupContext value={{ state, dispatch }}>
            <PopupLayout />
            {children}
        </PopupContext>
    )
}

export function usePopupContext() {
    const context = useContext(PopupContext)

    if (context === undefined) {
        throw new Error("Providerを下で定義してください")
    }

    const state = context.state
    const dispatch = context.dispatch

    return { state, dispatch }
}

上記ソースコードにはundefinedがあります。

export const PopupContext = createContext<popupContextType | undefined>(
    undefined
)

一応エラーハンドリングはしていますが上記コードを改造していこうと思います。
大規模なアプリになればなるほどエラーハンドリング抜けが出てしまし、思いがけないバグが起こります。今後そのようなことが起こらないようにまずは簡単のプログラムで試してみます。

Result型の作成

今回はエラーハンドリングがあるためResult型を作成していきます。
Result型は以下の通りです。

export const RESULT_OK = "ok" as const
export const RESULT_ERROR = "error" as const

export type Result<T> =
    | {
          kind: typeof RESULT_OK
          value: T
      }
    | {
          kind: typeof RESULT_ERROR
          err: Error
      }

Result型のデータを作成するオブジェクトを作成

Result型のデータを作成するオブジェクトを作成しておきます。

export const createResult = {
    ok: <T>(value: T): Result<T> => {
        return {
            kind: "ok",
            value: value
        }
    },
    err: (message: string): Result<never> => {
        return {
            kind: "error",
            err: new Error(message)
        }
    }
}

これを作成するのは任意ですが、作成しておくとミスは減るかなと思います。

undefinedResultに変換

作成したResultを実際に対象のソースコードに埋め込んでみましょう。

"use client"

import { createContext, ReactNode, useContext, useReducer } from "react"
import { popupContextType } from "./popup.types"
import { popupInitialState } from "./popup-initial-state"
import { popupReducer } from "./popup-reducer"
import PopupLayout from "@/layout/popup-layout"

+ export const PopupContext = createContext<Result<popupContextType>>(
+     createResult.err("Providerを下で定義してください")
+ )
- export const PopupContext = createContext<popupContextType | - undefined>(
-     undefined
- )

interface Props {
    children: ReactNode
}

export function PopupProvider({ children }: Props) {
    const [state, dispatch] = useReducer(popupReducer, popupInitialState)

+   const result = createResult.ok({ state, dispatch })

    return (
+       <PopupContext value={result}>
-       <PopupContext value={{ state, dispatch }}>
            <PopupLayout />
            {children}
        </PopupContext>
    )
}

export function usePopupContext() {
    const context = useContext(PopupContext)

+   if (context.kind === RESULT_ERROR) {
+       throw context.kind
+   }
+   const state = context.value.state
+   const dispatch = context.value.dispatch
-   if (context === undefined) {
-       throw new Error("Providerを下で定義してください")
-   }
-   const state = context.state
-   const dispatch = context.dispatch

    return { state, dispatch }
}

差分は上記のようになり、出来上がったソースは以下の通りになります。

popup-context.tsx
"use client"

import { createContext, ReactNode, useContext, useReducer } from "react"
import { popupContextType } from "./popup.types"
import { popupInitialState } from "./popup-initial-state"
import { popupReducer } from "./popup-reducer"
import PopupLayout from "@/layout/popup-layout"
import { createResult, Result, RESULT_ERROR } from "@/utils/global.typs"

export const PopupContext = createContext<Result<popupContextType>>(
    createResult.err("Providerを下で定義してください")
)

interface Props {
    children: ReactNode
}

export function PopupProvider({ children }: Props) {
    const [state, dispatch] = useReducer(popupReducer, popupInitialState)

    const result = createResult.ok({ state, dispatch })

    return (
        <PopupContext value={result}>
            <PopupLayout />
            {children}
        </PopupContext>
    )
}

export function usePopupContext() {
    const context = useContext(PopupContext)

    if (context.kind === RESULT_ERROR) {
        throw context.kind
    }

    const state = context.value.state
    const dispatch = context.value.dispatch

    return { state, dispatch }
}

こうすることで思わぬバグやエラーを防ぐことができます!!

まとめ

今回はnullやundefinedが危険と言われている件について、そしてnullやundefinedを削除したコードを作ってみました。

簡単なソースコードだとなかなか恩恵を受けることは少ないかなと思いますが、大規模になるとバグを探す時間が増しコストを大幅に無駄遣いしてしまうといったことになってしまうので、今のうちから意識しておくべき内容ではないかと思い今回記事にしました。

ではでは。

Discussion