Help us understand the problem. What is going on with this article?

Reactビギナーズガイドをtypescriptで勉強し直してわかったこと⑤【意外と困ったnullとundefinedの使い分け】

More than 1 year has passed since last update.

optionalにしますか?それともnullで初期化しますか?

例えばこんなコンポーネントがあったとして

import * as React from 'react';

interface Props {
  name: string;
}
interface States {
  name: string;
  age: number | null;
}

class Human extends React.Component<Props, States> {
  constructor(props: Props) {
    super(props);
    this.state = {
      name: props.name,
      age: null
    };
  }
  // ...
}
export default Human;

という風に、this.state.ageにはoptionalをつけず、とりあえずnullとして初期値を入れてしまう方がいいのか、

// 前略

interface States {
  name: string;
  age?: number; // 未定義の場合はundefined
}

class Human extends React.Component<Props, States> {
  constructor(props: Props) {
    super(props);
    this.state = {
      name: props.name
    };
  }
  //...
}
//後略

とstateにoptionalをつけてnullではなくundefinedとした方がいいのか悩みました。
es5で書いていた時はそんなこと一切悩まず、その時の気分で「なんとなく変数やプロパティの宣言だけしてnull入れておくか」とか、軽い気持ちでやっていました。
「Reactビギナーズガイド」に書かれているコードは頻繁にnullでstateの初期化をしていたのですが、typescritに書き直しているうちに、「typescriptだと明示的にnull書かなければundefined扱いになるしnullの必要性ってあるんだっけ?」という疑問が湧き起こり、調べてみることにしました。

nullは使うな!?

(下記リンクはtypescriptのコントリビューターに向けたガイドラインです。)
https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#null-and-undefined

typescriptのgithubのコーディングガイドラインに短くはっきり書いてありました。
typescriptのコンパイラでは基本的にundefinedのみを使おうとしているようです(typescriptでnullが使用できないということではありませんし、typescriptのユーザに「nullを使ってはいけない」というtypescript内部の方針を押し付けているわけでもありません)。
でもそれだけ書かれていても、なんで?って思ってしまいますよね。

こちらはちょっと古いですがダグラス氏のyoutube動画
https://www.youtube.com/watch?v=PSGEjv3Tqo0&feature=youtu.be&t=9m21s
null断ち宣言していますね。

こちらはTypescript deep diveの記事
https://basarat.gitbooks.io/typescript/docs/tips/null.html
nullの良くないことを紹介しつつundefinedのチェックに==を使うと、いいと書いてあります。

console.log(null == undefined); //-> true

こうするとnullがundefinedのシンタックスシュガーのように扱えるとのことですが、tslintで===にしろと怒られます(tsling.jsonのtriple-equalsの設定値によります)

そもそもjavascriptにnullとundefinedという、「なんでもないもの」を表す2つの型(値)があることが諸悪の根源だという見方もあるようです。(前述のダグラス氏)

undefinedはjsのエンジンが自動的に値を代入していない未定義変数に割り当てる値であるのに対して、nullは明示的に代入しないと使われることはありません(例外もありますが)。
両者とも全ての変数の型に割り当てることが可能な値であって、どちらかしか使うことが許されないとした場合、undefinedは自動的に値を代入していない変数に割り当てられ使わざるをえないことから、nullを捨てるという選択になるようです。

Netscapeのころからnullとundefinedはあるようですが、どういう経緯でnullとundefinedという二つのボトムの型が必要という判断になったのか。
気になりますがうまく調べられませんでした。

確かにnullとundefinedとの使い分けだの、判定方法の違いだので、非常に多くの人が悩んだり間違えたりして多くの時間を取られてきたこと(自分のこと)を思うと、決してjavascriptは初心者に優しい言語ではないし過剰な言語設計であるようにも感じます。

null使いたくないけどReact側がnullを返してくる

というわけで、ダグラス氏の意見を参考に軽い気持ちでnull断ちしてみようと思ってtypescriptの内部の方針に倣ってnullを使わないよう、「Reactビギナーズガイド」のコードをtypescriptで書いていると早速壁にぶち当たりました。

Reactのrefを使おうとした場合、typescriptでは次のようにフィールド変数などにrefの値を格納しなくてはいけないのですが、型としてはMyComponent | nullが返ってきてしまいます。これはどうあがいても変えようがありません。

private myComponent:MyComponent | null;

//...

render() {
  return(
    <MyComponent 
      ref={(el) => {this.myComponent = el; }}
    />
  );
}

こうなってしまうと、nullをコード内に書かなくてはなりません。
おそらくこれはrefの関数の仮引数ではカスタムコンポーネントだけではなくDOM要素を扱うこともあり、DOM系の組み込みのメソッド(document.getElementById()など)がnullを返すことに起因している返り値なので、妥当と言えば妥当です。

(でもなぜdocument.getElementByIdは指定のDOM要素が見つからなかった時にundefinedでなくてnull返すのか、これまで全く疑問にすら思わなかったことが疑問に思えてしまいました。
深追いすると勉強がすすまないので誰かが教えてくれるのを待ちます...)

nullとundefinedのtype guard

// 基本型か(undefinedでないか)
if (typeof hoge === "string") {
  const len = hoge.length;
  // ...
}

// nullでないか
if (hoge !== null) {
  const len = hoge.length;
  // ...
}

// コンポーネントまたはインスタンスか(undefinedでないか)
if (hoge instanceof MyComponent) {
  const len = hoge.length;
  // ...
}

es5で育ってきた身としては、もう少し簡便にできればいいのにと思ってしまいます...
特にrefsにカスタムコンポーネントやHTMLElementを配列で入れてforEachする時なんかは分岐処理分けないといけないから面倒だ、と思っていたら、typescriptにはnon-null assertionというものがあることがわかりました。

non-null assertion

non-null assertionを行う際は、オブジェクトのpostfixに!をつければnullでもundefinedでもアサーション(キャスト)できるようです(ただnull安全かどうかという観点では使えません!)。

仮に配列の要素(hoge)全てlengthプロパティを持っていたとするとループの中でifでtype guardしなくても

const len = hoge!.length;

と書くことができます。
でも実行時にしかnullが渡されるというエラーに気づけなくなってしまうので、null安全という大きな保証を失うことも考慮しなくてはなりませんね。

まとめ

  • javascriptには「なんでもないもの(状態)」を表す2つの型(nullとundefined)がある
  • javascriptの「なんでもないもの(状態)」は2ついらない、という見解がある
  • typescriptのコンパイラでは基本的にはnullは使われておらずundefinedのみ使われている
  • javascriptのundefinedはjsエンジンが未定義変数に使ってしまうことから、今後も使わざるをえないため、nullを捨てるという選択になる
  • いくらtypescriptを使っていてもnullを完全に断つのは不可能(にちかい)
  • non-null assertionはポリモフィックに処理するには長い分岐を書かなくて便利
  • non-null assertionを使用することによりnull安全の保証を失うことを考慮すべき

nullやundefinedについて、そんなに違いを意識することはなくこれまで普通に使ってきましたが、なぜ2つも同じような意味のプリミティブな型があるんだろうとは思ったことがありませんでした。
あまりにも当たり前すぎて、そういうものとしか考えてなかったです。
でもそういうのを考え直してみるのも一ついい機会になりました。
また、null不要論がありますが、現実はレガシーなコードでたくさんnullが使われていることを考えると、完全になくすことはほぼ不可能に近いと思いますので、そのプロジェクトの性質に応じて臨機応変に方針を決めていくことになると思います。
結局、使い分けはプロジェクトの性質次第という曖昧な結論ですが、また今後勉強を進めていく上で何かわかればまとめようと思います。

HiroshiAkutsu
現在、looking up株式会社(代表取締役)。前職は、マーケティングリサーチ会社にてシステム責任者。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away