TypescriptでRustに出てくるOption型を再現してみる


最近Rustの勉強から遠ざかってしまっていますが、Optionの話はかなり賢明に覚えています。 このOptionを取り入れてみたいと思い、Typescriptで再現してみました。

nullは「10億ドルの損失」と言われた

RustのOptionの話をするにあたって、まずはnullについて触れておきたいと思います。 Optionの話はThe Rust Programming Languageでは、enumの章で出てきますが、nullの話を少し触れています。

最近作られた言語ではnullを見かけなくなりました。

nullを作った人はTony Hoareという人です。 Tony Hoareはクイックソートアルゴリズムで有名な人です。

HoareはALGOL Wにnullを導入しました。 理由は、

simply because it was so easy to implement

つまり「実装が単純だったから」だそうです。

しかし、無数のエラーや脆弱性、そしてシステムクラッシュを引き起こすことになりました。

彼はnullを「billion-dollar mistake」と呼び、nullを作ったことを後悔しました。

詳しくは以下の文献を参照してください

NULL, "The Billion Dollar Mistake", Maybe Just Nothing Tony Hoare, the creator of NULL, now refers to NULL as The Billion Dollar Mistake. Even though NULL... dev.to

Null Pointer References: The Billion Dollar Mistake You have a function that takes 3 parameters, but 2 of them are optional. If you call that function with 2 parameters, does that make that… hinchman-amanda.medium.com

nullが引き起こす問題の例

私が知る限りのnullが引き起こす問題の例を挙げてみます。

NullPointerException

例えば、Javaをやっている人がいたら遭遇したことはあるかもしれないです。NullPointerException(よく「ヌルポ」と呼ばれるエラー)です。

String str = null;
System.out.println(str.length());

このコードはNullPointerExceptionを引き起こします。 Javaはコンパイルをしますが、上記のコードはコンパイル時にエラーにはなりません。 結果、実行した時にエラーとなりプログラムを止めてしまいます。

個人的には、コンパイルの段階でエラーを出してほしいと思っています。

またこれはJavaに限った話ではなく、Javascriptでも同様のエラーが起こります。

let str = null;
console.log(str.length);

Javaでは最近はOptionalを使ってnullを回避することができますし、Javascript系統でもTypescriptを使用し null許容等をすることで回避することが可能です。

しかし、Typescriptであっても罠はあります。

Typescriptにおけるnullの罠

仮にTypescriptを使っていたとしてもハンドリングを誤るとバグが起こってしまいます。 優秀なプログラマーなら別に大したことはないと思いますが、以下のようなケースだとバグが起こる可能性があります。

function getStatus(num: number | null): string {
  if (!num) {
    return "num is null";
  }

  return `num is ${num}`;
}

このコードは、numnullのときだけでなく、0のときも"num is null"を返してしまいます。

繰り返しますが、優秀なプログラマーであればこのようなミスはしないかもしれませんが、nullを扱うときはこのような罠があることを知っておく必要があります。 さらには、外部のライブラリを使っている際に、定義上はnullとなっていないのに何故かnullが返ってきてしまうこともあります(react-hook-formとか)。 単体テストを書いていればバグには気づくと思うので修正はその時点で気づくと思いますが、書いていないとバグの原因を探すのに時間がかかってしまいます。

RustのOption型

Rustはnullを持たない代わりに、Optionという型を提供しています。 Optionは、値が存在するかどうかを表す列挙型で、以下のように定義されています。

enum Option<T> {
    Some(T),
    None,
}

Optionは、値が存在する場合はSome(T)で表され、存在しない場合はNoneで表されます。

Rustはコンパイルが認識できるような形で表すことを考えました。 私はとてもこれは良い考えだと思いドキュメントを読んだ時に感動しました。

TypescriptでOption型(のようなもの)を再現

const basic = {
  OPTION_SOME: "some",
  OPTION_NONE: "none",
} as const;

interface Some<T> {
  readonly kind: typeof basic.OPTION_SOME;
  readonly isSome: true;
  readonly isNone: false;
  readonly value: T;
}

interface None {
  readonly kind: typeof basic.OPTION_NONE;
  readonly isSome: false;
  readonly isNone: true;
}

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

今後の拡張のために、isSomeisNoneのようなプロパティも追加しました。

nullundefinedを許さないようにしているのでNonNullableを使っています。

ちなみに、

type NonNullable<T> = T & {};

らしいです。

Option型を作成する関数

export const optionUtility = (function () {
  const { OPTION_SOME, OPTION_NONE } = basic;

  const createSome = <T>(value: NonNullable<T>): Option<T> => {
    return Object.freeze({
      kind: OPTION_SOME,
      isSome: true,
      isNone: false,
      value,
    });
  };

  const createNone = (): Option<never> => {
    return Object.freeze({
      kind: OPTION_NONE,
      isSome: false,
      isNone: true,
    });
  };

  const optionConversion = <T extends NonNullable<unknown>>(
    value: T | null | undefined,
  ): Option<T> => {
    if (isNull(value) || isUndefined(value)) {
      return createNone();
    }

    return createSome(value);
  };

  return Object.freeze({
    createSome,
    createNone,
    optionConversion,
  });
})();

毎回Optionのオブジェクトを作るのは大変なのでこのような関数を作っておきました。 即時関数にした理由はその時の気分で書いていたので特に理由はありません。

作った時はこれで良いと思ってましたが、なんか違う気もしたので今後作り直すことがあると思います。

使い方

const value1: Option<number> = optionUtility.createSome(42);
const value2: Option<string> = optionUtility.createNone();
const value3: Option<string> = optionUtility.optionConversion(
  process.env.SOME_ENV_VAR,
);

if (value1.isSome) {
  console.log(value1.value); // 42
}

if (value2.isNone) {
  console.log("value2 is none"); // value2 is none
}

if (value3.isSome) {
  console.log(value3.value); // SOME_ENV_VARの値
} else {
  console.log("value3 is none"); // SOME_ENV_VARがnullまたはundefinedの場合
}

使い方としては上記のようになります。 特に環境変数などはundefinedの可能性もあるのですが、ハンドリングを怠ってしまうとバグの原因になってしまうので個人的には良いやり方かなと思ってます。

メリット

メリットとしては、ハンドリングの強制力があることです。 nullでもハンドリングをすれば良いのですが、忘れることもありますしvalue != nullと書かずに!valueと書いてしまうこともあり、そこでバグを起こすこともあるかと思います。 nullだけではなくundefinedも同様のことが起こりうるでしょう。 とはいえ、正式な値を取得するにはisSomeisNoneを使う必要があるのでミスは減りますし、コードが明示的で個人的には好きです。

プロジェクトによってやり方は変わってくるとは思いますし、Typescriptを使わなくてもフロントエンドは書けるので、必ずしもこのやり方が正しいとは思いませんが、バグを少なくする1つの方法としては良いのではないかと感じています。

まとめ

今回は、RustのOption型をTypescriptで再現した話でした。 ハンドリングを怠るとバグの原因になります。 ハンドリングを強制する仕組みをあらかじめ作っておくことで、ハンドリングの怠りといったことはなくなるでしょう。

とはいえ、これは個人的にいいなと思っているものなので参考にするものしないもあなた次第です。