TypeScriptのstrict オプションは最初から有効にしたほうが良いという話

はじめに

はじめまして、イタンジ株式会社で内装工事くんというプロダクトを開発している櫻井です。 最近見たおすすめの映画は「キャッチ・ミー・イフ・ユー・キャン」です。

早速ですが皆さん、TypeScriptは使っていますか? TypeScriptには型に関するいくつかのオプションがあり、その全てのオプションを有効化するstrict オプションがあります。 strict オプションを有効化すると、TypeScript のコンパイラによる型チェックを厳しくして、より安全なコードを書くことができます。 私が所属しているプロダクトではTypeScriptを使っているのですが、一部オプションを無効化している箇所があり、TypeScriptの恩恵を充分に受けられていない状態だったので、全てのオプションを有効化しようという運動が行われています。

この記事では、なぜstrictオプションを最初から有効化するべきなのかについての理由と、私がプロダクトで有効化した2つのオプションについて紹介します。 TypeScript の version は 4.7.3 です。

最初から有効化するべき理由

  • リファクタリングしやすくなる
  • 技術負債になる
  • どんな人が入ってきても、バグを未然に防げる
  • 後から有効化するのが大変

compilerOptions の中身

# tsconfig.json
  "compilerOptions": {
    "target": "es5",
    //TypeScript4.4時点でstrict: trueにするとすべてのオプションが有効になる
    "strict": true,
    // "strict": trueにしていても下記のように個別で無効化できる
    "noImplicitAny": false,
    "strictNullChecks": false,
  },

※もっとたくさんのオプションがあるので、詳しくはこちらをご覧ください。

noImplicitAny オプションを無効化するとどうなるのか

このオプションを無効化すると、型アノテーションのない関数や変数に対して暗黙の any を許容することができてしまいます。例えば、下記のようなコードを見てみましょう。

const double = (value) => value * 2;
// 型アノテーションのないvalueはany扱いとなる

console.log(double("123"));
// 123という文字列を渡せてしまう。

double 関数に渡されている value という引数は、型アノテーションがないため any 型として扱われます。 any はどんな型が入ってきても平気なため、 string や boolean など違う型を渡してもエラーになりません。 では有効化してみるとどうでしょうか。下記コードを見てみましょう。

const double = (value) => value * 2;
// Parameter 'val' implicitly has an 'any' type.

console.log(double("123"));

ここで value という引数は暗黙的な any として扱われているというエラーが出ます。 このエラーに遭遇したあなたは何言っているんだと、value の型は number 以外ありえないだろうと、下記コードのように修正するでしょう。

const double = (value: number) => value * 2;
// valueに型numberを明記

console.log(double("123"));
// 型 'string' の引数を型 'number' のパラメーターに割り当てることはできませんというエラーが出る

するとどうでしょうか、あなたが value は number 型しか入らないに決まっているだろうと思っていたはずなのに、別の double 関数を実行している場所では string が渡っていることに気づけるではないですか。 このように、noImplicitAny を有効化すると、型アノテーションのない変数や関数に対して any を許容しなくなり、コードの安全性を高め、予期せぬバグを防ぐことができます。

strictNullChecks オプションを無効化するとどうなるのか

次は strictNullChecks オプションについてです。 このオプションを無効化すると、null と undefined をどの型にも代入できてしまい、かつオブジェクトが null や undefined の場合もお構いなしにプロパティへアクセスすることを許してしまいます。 下記のようなコードを見てみましょう。

const [age, setAge] = React.useState<number>(null);
// numberがはいるはずなのnullを代入している

const double = (value: number) => value * 2;

console.log(double(age));
// ageはnullなので結果は0になるが教えてくれない
// undefinedの場合はNaNになる

age という state には number という型を明記したはずなのに、double 関数実行時にエラーが出ないことはもちろん、age の初期値に null が代入されていることに対してのエラーも出ません。 これは、オプションを無効化することでどんな型に対しても null や undefined を渡すことができてしまうようになるからです。 age の state をどこかで更新し忘れていたり、更新する関数のコードが間違っていたり、挙動がおかしくても気づくことはできないのです。 では、strictNullChecks を有効化してみるとどうでしょうか。下記コードを見てみましょう。

const [age, setAge] = React.useState<number>(null);
//型 'null' の引数を型 'number | (() => number)' のパラメーターに割り当てることはできません。

const double = (value: number) => value * 2;

console.log(double(age));

age の初期値に対してエラーが出ています。 型アノテーションのついた変数や引数には null や undefined を代入することはできなくなります。 次に age の型を null を許容するように変更してみましょう。

const [age, setAge] = React.useState<number | null>(null);
// エラーはなくなるが関数実行時にエラーが起きる

const double = (value: number) => value * 2;

console.log(double(age));
//型 'number | null' の引数を型 'number' のパラメーターに割り当てることはできません。

ここで初めて double 関数実行時に null が渡っていて、結果が0(もしくはNaN)になっているということに気づく事ができます。 この場合の修正例は以下です。

const [age, setAge] = React.useState<number | null>(null);

const double = (value: number | null) => {
  if (value === null) {
    return null;
  }
  return value * 2;
};

console.log(double(age));

次に、オブジェクトが null や undefined の場合にプロパティへアクセスすることを許してしまうとどうなるのか見てみましょう。

type User = {
  id: number;
  name: string;
  age: number;
  description?: {
    hobby: string;
  } | null;
};

export const SamplePage = () => {
  // APIを叩いた時に帰ってくるデータ
  const users: User[] = [
    {
      id: 1
      name: "taro",
      age: 20,
      description: { hobby: "sauna" },
    },
    {
      id: 2
      name: "jiro",
      age: 30,
    },
  ];

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <p>{user.name}</p>
          <p>{user.age}</p>
          <p>{user.description.hobby}</p>
        </div>
      ))}
    </div>
  );
};

jiro君は自分の趣味を登録していないため画面を開いた時にTypeError: Cannot read properties of undefined (reading 'hobby')というエラーが出ます。 有効化するとどうでしょうか。コードは割愛します。

 <p>{user.description.hobby}</p>
 //波線をフォーカスするとオブジェクトは 'null' か 'undefined' である可能性があります。

このようにnullかundefinedになる可能性のあるコードを発見してくれます。 今まで紹介してきた潜在的なエラーとは違い、nullやundefinedの場合のコードを書かずにリリースしてしまうと、ユーザーがエラー画面を見ることになってしまい、なかなか致命的です。

しかし、strictNullChecksを有効化すると、nullやundefinedの代入やオブジェクトが nullやundefinedの場合は教えてくれるので、またしてもコードの安全性を高め、予期せぬバグを防ぐことができるのでとても嬉しいです。ちなみにこの場合はhobbyを登録している場合のみ表示させればいいのでこうなります。

 {user.description && <p>{user.description.hobby}</p>}

最後に

如何だったでしょうか? 今回紹介した2つのオプションについてと、紹介できなかったオプションを全て含めたstrictオプションを有効化するべき理由について少しでも伝われば幸いです。 オプションを有効化することで覚えることが増えたり、少し記述量が増えてしまったり、開発スピードが落ちてしまったりと色々デメリットがでてきますが、それ以上にメリットがあると思います。 プロダクトが成長しすぎて今更そんなことしてもなぁと思っているそこのあなた、今こそ全てのオプションを有効化するときが来ました。 先延ばしにすればするほどどんどん修正範囲が広くなってしまうのがとても辛くなるので、最初から、または気づいたときから少しずつエラーを解消していくことをおすすめします。

最後まで読んでいただき、ありがとうございます。 皆さんのTypeScript生活がより良いものになることを応援しています!