🌊

浅瀬チャプチャプ勢でも享受できるReact + TypeScriptの恩恵

2021/03/16に公開

はじめに

今回、初めて記事を投稿させていただきます。今回は「浅瀬チャプチャプ勢でも享受できるTypeScriptの恩恵」と題しまして、「エンジニアってわけじゃないけど React + JS とかとかはかけるよ」 という人向けに、私が書いていて気がついたTypeScriptの恩恵のお話をしたいと思います。おそらくですが、ガチモンのプロダイバーの皆さまが閲覧した場合、稚拙な内容に伴うストレスから 「記事がサンドバックに見えてくる」 「投稿者に石をぶつけたくなる」 などの症状が出ると思いますが、仕様です。物理攻撃に関してはぐっとこらえていただきますよう、お願い申し上げます。

今回のポイント

今回のポイントは以下の2つです。

  1. Typescriptを使うのは有能なエディターに助けてもらうため
  2. 型指定はそこまで気にしなくてもいいと思うよ。←やめて、叩かないで!

…イ、イタタタ… ボロッ
…そ、その、ほら、言葉の文ってやつですよ…。
では、早速本題へ…。

ことの発端

ある日、現在運営中のアプリ開発の話をしていると、ふとこんな話題が…。

TypeScript採用してるけどさ、メリットよりも型指定の辛さとか学習コストのほうが上回ってない?

…うん、そうなんですよね。
 特に私のような浅瀬の民からすると、TypeScriptの恩恵って巷で言われるよりも遥かに小さい。確かに大規模なチーム開発などでメンテナンス性を高めようと思ったとき、フロントの言語として真っ先に選択肢あがるのはTypeScriptでしょう。

ですが一方で、小規模なアプリ開発やサイト制作などにおいては、「TypeScriptで書くと型指定を調べたり記述したりするのが時間かかって面倒だし、情報もJavaScriptのほうが多いからJavaScriptでいいか」なんてことが起こるわけです。同時に、TypeScript特有の型に関するエラーで時間を取られることが多いことも、こんな意思決定に拍車をかけていると思います。 もちろん、その手間をかけるメリットがTypeScriptにはあります…あるはずです。じゃなければこんなに普及しませんよね。ですが、そのメリットを芯まで感じ取れない人も少なくないのではないでしょうか。正直な話、私もその一人です。

GatsbyJsを利用中に起こった感動の物語

そんな私ですが、最近 GatsbyJs with TypeScript で個人のブログを作りました。ちなみに、GatsbyJsっていうのは最近流行りらしいSSG−−静的サイトジェネレーターってやつです。WordPressにあまり魅力を感じなかった私は、個人でチャプチャプ使っていたReactを制作に活かせないかなぁ、なんて妄想していたんですが、趣味勢の妄想はガチ勢の過去です。ありました、そんな神のシステム。詳しくはこちら
 さて、「メリットわからないとかいいながらつかうとか、お前サイコパスかよ?…もしかして…マゾ?」そう思った皆さま、ご安心ください。私は年上のお姉さんが好みですが、決してサイコパスでもマゾでもございません。この意思決定には、GatsbyJsを触ってみて気がついた、TypeScriptの私にとっての最大のメリットが大きく働いています。

Gatsby Imageとコンポーネントの再利用

このGatsbyJsが高い評価を受ける最大の理由の一つがこのGatsby Imageの存在です。この子は「サイト表示時に重さの原因になる画像データをよしなにしてくれる」という代物です。(現在は非推奨。Gastby Plugin Imageっていうさらにいいやつに置き換わってますので注意。)う〜ん、「よしなに」ってあたりが実に私好み。
 しかしこのGastby Image、複数の画像を表示するためにいちいち設定を書かなくてはいけないという問題が…。これではReactの最重要要素の一つ、コンポーネントの再利用が効かないではないかね…。もちろん、そんな単純な問題にはすでに回避策が出されています。こちらの記事に書いてあるんですが、先に全部の画像データを机に広げて、必要なものを都度切り貼りしようって作戦です。こうすると、image.jsxにまとめておけば各ページ、コンポーネントから再利用できるよねと。一見するとReactとGatsbyJsの勝利です。「…やったか?」

先人の知恵にも落とし穴が…

私も先の記事を参考にこんな記述をしてコンポーネントを再利用できる状態に持っていきました。

import React from 'react';
import {useStaticQuery, graphql} from 'gatsby';
/* このプラグインは非推奨です。感動体験をそのままかいてるのでご容赦を。 */
import Img from 'gatsby-image';

const Image = props => {
   /* graphqlってやつです。今回は気にしなくても差し支えありません。 */
   const data = useStaticQuery(graphql`
   query {
       images: allFile {
         nodes {
           relativePath
           name
           childImageSharp {
             fluid(quality: 100) {
               ...GatsbyImageSharpFluid
             }
           }
         }
       }
   }`)

   /* この辺がアブナイ! */
   const image = data.images.nodes.find(n => n.relativePath.includes(props.fileName))
   if (!image) { return null; }

   return <Img fluid={image.childImageSharp.fluid} alt={props.alt} />
 }

 export default Image;

はい、お気づきの方もいらっしゃるでしょう。これ、imageがfalseになりうるんですよね。当然、先の記事の作者様もその辺は織り込み済み。直後にif (!image) { return null; }を記述することによって、エラーを回避されています。ので、そのまま使わせてもらいました。ありがたや…。ですがこの場合、画像が表示されてないページ−nullがリターンされているページがないか、自分でチェックする必要があります
 ところで、私の目は「節穴」として高い評価を受けており、モノ探しにおいての信用は地に落ちております。そんな私が当該箇所を見つけることができるのでしょうか。…残念ですが、見落としが乱舞することは想像に難くありません。「まぁ、ミスしないように気をつけて、あとconsole logでも出せば…」そう思った皆さま、実は先のコードには根本的なミスの温床が…
 そもそもこのコード、「fileNameとして渡した名前を含むnodeをimageに入れるよ」 って内容じゃないですか。と、いうことは 「fileNameをTypoしたらその瞬間にnull確定」 ってことです。…ゑ?そんなん絶対ミスるやん…。
 なるほど!これこそが TypoScript ってことか!
 …あ、まって、審議しないで!ウワァァァァァァ−−こうして、私は慟哭とともに地獄へ叩き落とされたのでした…。

顕現せり一本の蜘蛛の糸、TypeScript

しばらくして、血の池を平泳ぎする私の前に一本の蜘蛛の糸が現れました。それこそが今回の主役、TypeScriptです。なんと、TypeScriptを使うだけでTypoを未然に防ぐことができるんです!な、なんだって〜?
先程のコードをTypeScriptで書くとこんな感じになります。

import React from 'react';
import {useStaticQuery, graphql} from 'gatsby';
import Img from 'gatsby-image';

type PropsType = {
    /* ここがポイント! */
    fileName: 'foo.jpg' | 'bar.jpg' | 'baz.jpg';
    alt: string;
};

const Image: React.FC<PropsType> = props => {
    const data = useStaticQuery(graphql`
    query {
        images: allFile {
          nodes {
            relativePath
            name
            childImageSharp {
              fluid(quality: 100) {
                ...GatsbyImageSharpFluid
              }
            }
          }
        }
    }`)

    const image = data.images.nodes.find(n => n.relativePath.includes(props.fileName))

    return <Img fluid={image.childImageSharp.fluid} alt={props.alt} />
  }

  export default Image;

今回の最重要ポイントは PropsTypeで予め入っている画像の名称一覧を固めちゃおう! というアプローチです。これによって、もしTypoがあった場合は 入力した箇所でエディターがtype errorを出して教えてくれます! 具体的には「hoge.jpgなんてファイル名はリストにないよ!」って指摘してくれる感じです。実際にこの書き方を導入してからTypoによるエラーへのエンカウント率は激減。いや〜、VScodeは有能だなぁ。これによって、コンポーネントを安全に再利用できるようになりましたとさ。めでたしめでたし。

「…だからなんだよ?」と思われた非Gatsbyユーザーの皆さま、武器をお収めください。次章にてReactユーザーが受けられる普遍的な価値(…と思われるなにか)をご提案いたします。

ReactをTypeScriptで書くと祝福を受け取れる例

私が思うに、「componentに渡されうるデータがおおよそ決まってるとき」 なんかは浅瀬の民も恩恵に与ることができるのではないでしょうか。先程の例も然りです。別の例としては性別選択なんかが挙げられるでしょうか。
ということで、ちょっと書いてみました。以下のようなデータ一覧から「特定の性別をもつスマブラSPのファイターのリスト」を取得する場合を考えてみましょう。(例としてはもっと適切なものがあると思いますが、深海からの攻撃はお控えいただけますと、主に私のガラスのハートが助かります。)

/* DummyData.json */

[
    {
        "name": "クラウド",
        "gender": "man"
    },
    {
        "name": "リュウ",
        "gender": "man"
    },
    {
        "name": "ゼルダ",
        "gender": "woman"
    },
    {
        "name": "ベヨネッタ",
        "gender": "woman"
    },
    {
        "name": "ポケモントレーナー",
        "gender": "both"
    },
    {
        "name": "ベレト/ベレス",
        "gender": "both"
    },
    {
        "name": "ロボット",
        "gender": "none"
    },
    {
        "name": "パックンフラワー",
        "gender": "none"
    }
]

ご覧の通り、ファイターの名前と性別を入れただけのデータです。性別は4通り、「男」「女」「キャラ選択画面で男女を選択可能」「性別なし」です。

JavaScriptで書いた場合

まずはJavascriptの場合。一覧を表示するためのItem.jsxはこんな形でしょう。

/* Item.jsx */

import React from 'react';
import DummyData from '../DummyData.json';

const Item = ({title, gender}) => {
    const pickData = gender => {
        const fighters = DummyData.filter( data => data.gender === gender );

        return fighters
    };

    return (
        <div>
            <h1>{title}</h1>
            {pickData(gender).map(
                fighter => <p> {fighter.name} </p>
            )}
        </div>
    )
};

export default Item;

そして、これを使うApp.jsで読み取るとこんな感じでしょうか。

/* App.js */

import React from 'react';
import './App.css';
import Item from './component/Item';

function App() {
  return (
    <div className="App">
      {/* 正常に動く例 */}
      <Item
        title='First Item'
        gender='man'
      />
      <Item
        title='Second Item'
        gender='woman'
      />
      {/* 問題が発生する例: props.genderがない場合 */}
      <Item
        title='Third Item'
      />
      {/* 問題が発生する例: props.genderで渡す値が間違っている場合 */}
      <Item
        title='Third Item'
        gender='other'
      />
    </div>
  );
}

export default App;

してこのコード、表示はこうなります。

そうなんです、props.genderがない場合も、それが間違っている場合も、「エラーもなく、何も表示されない」 ということが起こるんです。今回は1ファイル内で、それも意図的に問題を起こしていますからなんの感慨もわきませんが、想像してみてください。実際にはこれが複数のファイルから再利用され、別々のページの表示に影響するんです。後々ミスが発覚した場合、どんな苦行が待っているかは推して知るべしでしょう。

TypeScriptで書いた場合

では、TypeScriptを使った場合はどうでしょうか。きっと2つのファイルはこうなると思います。

/* Item.tsx */

import React from 'react';
import './App.css';
import DummyData from '../DummyData.json';

type ItemPropsType = {
    title: string;
    /* ここで入力を限定 */
    gender: 'man' | 'woman' | 'both' | 'none';
}

const Item: React.FC<ItemPropsType> = ({title, gender}) => {
    const pickData = (gender: string) => {
        const fighters = DummyData.filter( data => data.gender === gender );

        return fighters
    };

    return (
        <div>
            <h1>{title}</h1>
            {pickData(gender).map(
                fighter => <p> {fighter.name} </p>
            )}
        </div>
    )
};

export default Item;
/* App.tsx */

import React from 'react';
import Item from './component/Item';

function App() {
  return (
    <div className="App">
      {/* 正常に動く例 */}
      <Item
        title='First Item'
        gender='none'
      />
      <Item
        title='Second Item'
        gender='both'
      />
      {/* 問題が発生する例: props.genderがない場合 */}
      <Item
        title='Third Item'
      />
      {/* 問題が発生する例: props.genderで渡す値が間違っている場合 */}
      <Item
        title='Fourth Item'
        gender='foo'
      />
    </div>
  );
}

export default App;

こちらの場合の表示は…とその前に、私が利用しているVSCodeの場合、ワークスペースの問題としてこんなことを教えてくれます。

…問題、そのまんまじゃん。そうなんです、TypeScriptでは必須の型がないときやその割り当てに問題があるとき、それを直接エラーとして教えてくれます。もちろん、表示でもこんな感じでエラーが出ます。


こんな感じのエラーがもらえる仕様によって、開発における些末なエラーをエディターでキャッチして教えてもらえるようになったんですが、これが意外と快適でして。私の節穴もお役御免ですな。それに、どのコンポーネントのどの部分でエラーが起こっているか把握しやすくなったことも、開発体験の大幅な向上につながったと信じてます。

ね、こうやって考えると意外と恩恵もありそうじゃないですか??

そんなこといったって、型指定の辛さは…。

「でも、TypeScriptの型指定は面倒じゃないですか。」…奥さん、逆ですよ。逆に考えるんです。型指定なんてしなくていいや と、そう考えるんです。
 ぎゃ〜〜、まったまった!と、とりあえず、その銃をおろそう!

浅瀬の民には「型推論」という浮輪がある

こんなことをいうと多方面から袋叩きにされそうで非常に怖いのですが、無理に型指定をしようとしてエラーにハマるぐらいなら、適当なところで妥協したほうがいいと思います。
 よく、静的型付け言語を外から見ていると「型を自分で指定しないと!」という「至極まっとうな」感覚に陥りがちですが、少なくともTypeScriptにおいてはデータからある程度型を推測してくれます。すごく端的な例ですが、これなんかそうだと思います(もっといい例がありましたらご教授ください…)。

これは先程のfighterを取り出すコードの一部です。見ての通り、なんの指定もしていないdataの中身が明確に規定されているではありませんか。このようにある程度自明のものであれば無理に型を書く必要はないんです。そこまで気をもまずとも、適当にやっておけばあとはよしなに整えてくれます。大丈夫、私たち浅瀬の民よりも、エディターとその開発チームのほうが数千倍優秀です。彼らの活躍を期待しましょう。

まて、childrenをまるまる渡したいときもあるだろ!

Reactをお使いの皆さまはお思いかもしれません、「そもそもさっきみたいに明確にpropsが規定されないことだってあるだろ!」「childrenをまるごと渡したいときなんて日常茶飯事じゃないか!」…そんなときにはTypeScriptの奥の手(禁じ手)を…使いましょう…。型にanyを指定すれば、全ての入力が許容されます。わからなかったら、anyを使って押し通しましょう。
 …ごめんなさい、本当はよくありません。Reactにもいろいろと型が指定されており、それをうまく使い分けたほうが明らかに安全性は高まります。ですので、できることなら可能な限り活用したほうがいいです。VSCodeなどではある程度型を教えてくれますしね。ま、無い袖は振れませんからね、諦めて一緒にゆるゆる勉強しましょう。

さて、有識者の皆さまには暴論とも取れる内容も散見される本稿、投稿者はタコ殴りにされておりますが、ひとつだけ言えることがあります。それは、TypeScriptと型指定を敬遠してJavaScriptを使い続けるよりは、ある程度anyに頼ってでもTypeScriptで書いていったほうが遥かに有益であるということです。開発者体験の向上は明らかですし、それは今後更なる発展の可能性こそあれ、状況が悪化することはありえません。私たちが頼りとする先人や巨人のライブラリも、サポートを進めることはあっても、それを排する体制を取ることは考えにくいと言えます。
 ともあれ、私たち浅瀬の民はプロダイバーの皆さまの活動を応援し、情報をキャッチアップしながら、私たちにあった砂山づくりの方法を探すのが無難でしょう。そのためのツールとしてのTypeScriptは、我々のような趣味レベルの人間でも使えるところまできていると思います。

まとめ

改めて、今回お伝えしたかったのは2つだけです。

  1. TypeScript + 優秀なエディターで、Typoなどの人為的凡ミスを未然に防ごう
  2. ある程度anyを使ってでもTypeScriptを使う価値はある、自分が辛いと思う部分を回避しよう

つまるところ、自分が間違えそうなところを助けてもらうためにTypeScriptを活かし、自分が辛くなるようなところは、TypeScriptの設定を緩めるぐらいのほうが付き合いやすいです。「JavaScriptかTypeScriptか」という選択よりも、「TypeScriptと楽しつ付き合うためにどう折り合いをつけるか」を考えたほうが、何よりも開発が楽です

閑話休題、ここまでお付き合いいただきありがとうございました。また浅瀬で貝殻を拾ったら報告する予定です。今後も生暖かい目で見守ってください。よろしくお願いいたします。

Discussion