🕌

TypeScript anyで逃げちゃう人がまず覚えるべきこと

2024/01/11に公開

概要

TypeScriptを使ってWebアプリ等を書いている際、コンパイルを通すために色々目を瞑って
anyasを乱用しながら仕事を進めていくうちに罪悪感が拭えなくなってきたため、基本に立ち返って勉強し直すことにしました。

私と同じようにanyで色々逃げがちな方がまず抑えるべきはここなんじゃないか、と思った点として、手始めに本記事で以下について簡潔に述べさせていただきます。

  • 型の大切さを改めて
  • anyのヤバさ
  • unknown
  • as

※ 本内容は以下書籍を存分に参考にさせていただいた上で、私の解釈も含めてかなり噛み砕いて記載させていただいております。
https://gihyo.jp/book/2022/978-4-297-12747-3

対象読者

  • TypeScript初心者~中級者
  • TypeScriptで動くものを書くことができるが、コンパイルエラーに目をつむってanyとかで逃げがちな方。
  • 「よーわからんけど動いたからいいや」が頻発している方
  • コードレビューなどで「TypeScriptらしいコードで書くならば。。。」といった感じで上から指摘したい方

「型」の偉大さ

「型」を丁寧に扱うとまず安心感が爆上がりする。 そしてコード記述量が増えてるはずなのに、多くの場合コーディングの正確さ、速度、読みやすさが格段に向上する。

  • 型安全性: コンパイルエラーにより型のエラーに瞬時に気づく。エディタがリアルタイムにエラーを教えてくれる。
    (テストピラミッドを拡張し、ユニットテストの下に静的コード解析を追加したモデルを見たことがある。とにかくエラーは可能な限りコンパイラでで弾くことが望ましい。)
  • エディタの補完機能によるコーディング加速、コーディングミスの軽減
  • 可読性の向上。 source code is a document.

anyはなぜ使ってはいけないか

  • 上記偉大さを放棄する行為だから。
  • 一度anyを使うと、その変数を引き継ぐ限り型の信頼性が崩壊する。
function example(data:any) {
  // 引数でany型で受け取っているので、ここで型付けしたところで、以降全く信用できない。
  const v: string = data;
  
  // stringじゃなければランタイムエラー
  console.log(v.length);
}

// こんなコードもコンパイル時に怒ってくれない。
example(null)

それでもanyを使う場面

  • javascriptのコードを移植する時。anyを使いながら段階的に進める戦略が取れる。

引数の型がわからないとき、anyを使う前にやること

  • まず第一にunknownを使うべし。
function example_err(data:unknown) {
  // ここでコンパイルエラーになってくれる。不明なまま使っている時は怒られることが正しい!
  // Type 'unknown' is not assignable to type 'string'.
  const v: string = data;
  console.log(v.length);
}

function example_success(data:unknown) {
    if (typeof(data) === "string") {
      // ポイント: このブロックでは data は "string" 型になっている!!
      const v: string = data;
      console.log(v.length);
    } else {
        console.log("string型ではありません")
    }
}

// もちろんランタイムエラーにならない。
example_success(null)
example_success("success!")

ポイントとしては、typeof を使ってif文でtypeを特定すると、そのブロック内で
コンパイラが(ランタイムでなく) dataの型を認識してくれること。

as(型アサーション)の使い所

as(型アサーション)を使うことによって型を強制的に変更することができる。コンパイルエラーを避けるために無理やり使いがちな機能だが、可能な限り使うことは避けるべき。
どうしても使わざるを得ないケースは、ロジックとして 「明らかにその型になるはずなんだけど、TypeScriptが理解してくれない」 場合。

type AdminUser = {
  tag: "admin";
  email: string;
}

type NormalUser = {
  tag: "normal";
  email: string;
  tel: string;
}

// 引数のusersが全てNormalUser型だったらtelのlistを返す関数。
function getTelNumbersIfAllNormalUsers (users: AdminUser[] | NormalUser []): string[] | undefined {
        // ここのif文で、userはNormalUser型と確定しているはずなんだけど。。。
        if (users.every(user => user.tag === "normal")) {

            // user.tel がAdminUserにはないと怒られコンパイルエラー
      return users.map(user => user.tel)
    }

    return undefined
}

このような場合は、asを使ってTypeScriptに型を教えてあげれば通る。
ただし、コメントをつけた上で慎重に使うこと。(しっかりユニットテストも行うべし。)

type AdminUser = {
  tag: "admin";
  email: string;
}

type NormalUser = {
  tag: "normal";
  email: string;
  tel: string;
}

// 引数のusersが全てNormalUser型だったらtelのlistを返す関数。
function getTelNumbersIfAllNormalUsers (users: AdminUser[] | NormalUser []): string[] | undefined {

        // ここのif文で、userはNormalUser型と確定しているはずなんだけど
       // TypeScriptさんはわからないみたいなので
    if (users.every((user) => user.tag === "normal")) {
     
       // asを使って型アサーション
      const normalUsers = users as NormalUser[]

            return normalUsers.map(user => user.tel)
    }

    return undefined
}

今後

まだまだ色々有用な知見が得られましたので、今後色々書いていきたいと思います。
ご指摘等、どしどしいただければ幸いです。

Discussion