TypeScript wayでReactを書く

TypeScriptはJavaScriptに静的型による型チェックを加えた言語で、Microsoftによって開発されています。TypeScriptの登場以降その人気上昇は留まるところを知らず、Webアプリの開発に採用される例も増えているようです。TypeScriptはJavaScriptと同じ構文・言語機能を持ち、唯一の違いは**型がある**点、つまり**型が書けて型チェックがある**というところです[^type_dynamic]。

[^type_dynamic]: ちなみに、筆者は「**型というのは静的型のことであり、いわゆる動的型は型のうちに入らない**」という宗派です。よって、「JavaScriptにも型はあるんですけど？ｗ動的型付け知らないの？ｗ」というようなつっこみは受け付けていません。悪しからずご了承ください。なお、漸進的型付けに関しては型システムを持つということで静的型の方に含めて考えています。

また、ReactはJavaScript向けのUIライブラリで、HTML要素やコンポーネントを第1級オブジェクトとして扱いUIを構築するスタイルが特徴的です。こちらはFacebookが開発しています。

この**最高のプログラミング言語**<font color="#aaaaaa">（個人の感想です。）</font>に**最高のライブラリ**<font color="#aaaaaa">（個人の感想です。）</font>を組み合わせるのがWebアプリ開発におけるまさに**唯一無二の解**であることはかけらも疑う余地が無いのですが<font color="#aaaaaa">（個人の感想です。）</font>、そのときに**TypeScriptの真価**を発揮させるのはそう簡単なことではありません。

そこでこの記事では**TypeScript way**と称して、TypeScriptの真の力を発揮させるための方法論、すなわち**型ファーストなTypeScriptプログラミング**を説明します。また、「型オタクの戯言ではなく実用的な話をしているんですよ」ということをアピールしたいので**React**を例に用いて説明します。Reactを知らない方には付いてくるのがやや難しいかもしれませんが、考え方自体はReact以外にも応用できるでしょう。

ちなみに、React本体にはTypeScript用型定義は同梱されていません。DefinitelyTypedで管理されている`@types/react`という型定義パッケージを使うことになります。さすが有名パッケージだけあって、これはかなり頑張って作られています。安心して使いましょう。

話を戻しますが、そもそもTypeScriptは**型の部分を除けばJavaScriptと同じ**という特徴を持つので<font color="#aaaaaa">（enumやnamespaceなどの黒歴史は見ないふりをしてあげましょう）</font>、TypeScriptでのコーディングというのは基本的にはJavaScriptを書くのと同じになると思われるかもしれません。つまり、JavaScriptコードを書くのと同じ考え方でTypeScriptコードを書くことができ、ついでにちゃんと型を書いてあげれば立派なTypeScriptコードの完成です。

しかしながら、このやり方は**JavaScriptファーストであり、型がオマケ扱い**です。それゆえに、実は**必ずしもこれがベストプラクティスとは限りません**。


**型ファースト**というのは、型があることを前提にプログラムを設計し、型の恩恵を最大限受けるための書き方をすることで達成されます。**型ファーストでプログラムを書くと、型のことを考えずに書いたJavaScriptプログラムとは異なる形になることがあるのです**。

それゆえに、JavaScriptからTypeScriptへの移行案件では、型ファーストを採用するのに「ただ型を書くだけ」という範囲を逸脱するリファクタリングが必要となり難しいかもしれません。どちらかといえば、この記事の内容は新規開発でTypeScriptを使用する際に力を発揮するでしょう。

そういう事情もありますから、**この記事の方法が唯一絶対の正解だ、などと主張するつもりはありません**。型ファーストなプログラムが最もTypeScriptの力を活かせるのは間違いありませんが、それには相応の苦労が伴います。TypeScriptの力をどれくらい活かすのか、それはTypeScriptを使う人が決めることなのです。

なお、今回の話は結局「型の力を活かそう！」という話ですので、型システムが強い言語に慣れ親しんでいる方にとってはそんなに目新しい話ではないかもしれません（特にADTを持つ言語）。主にJavaScript畑からTypeScriptに参入してきた人をターゲットにしているのでその点はご了承ください。

前置きが長くなりましたが、いよいよ本題に入りTypeScript wayなプログラムの書き方を説明していきます。いくつかのケーススタディを通して解説することにしましょう。

## データをロードする場合

Webアプリケーションで頻出のパターンは「APIからデータをロードして表示する」というものです。まず型のことは忘れてこれを作ってみましょう。ロードしたデータはコンポーネントのstateに保存しておくことにします[^state_store]。

[^state_store]: ここで「おいおいニワカか？　そういうのはストアに入れるもんだろｗ」と思った方はご安心ください。データをどこに入れるかは今回の話の本質には影響しません。今回は簡単のためにstateに入れますが、ご自分の好きな方式に適宜読み替えてください。

```jsx
class App extends React.Component {
  state = {
    isLoading: true, // 初期状態はロード中
    user: undefined, // ユーザーデータ
  };

  componentDidMount() {
    loadUserData()
      .then(user => {
        this.setState({
          isLoading: false,
          user,
        });
      });
  }

  render() {
    if (this.state.isLoading) {
      return <p>読み込み中</p>
    }
    // 読み込み済
    return <p>こんにちは、{this.state.user.name}さん！</p>;
  }
}
```

多分だいたいこんな感じになるかと思います。今読み込み中かどうかを表す`isLoading`フラグを用意しておき、読み込み終わったら`this.state.user`にそのデータを入れると同時に`isLoading`フラグを`false`にします。

では、これにそのままTypeScriptで型を付けてみましょう。これは型ファースト**ではない**例になります。

```tsx:型ファーストではない例
interface State {
  isLoading: boolean;
  user?: {
    name: string;
  }
}

class App extends React.Component<{}, State> {
  state: State = {
    isLoading: true, // 初期状態はロード中
    user: undefined, // ユーザーデータ
  };

  componentDidMount() {
    loadUserData()
      .then(user => {
        this.setState({
          isLoading: false,
          user,
        });
      });
  }

  render() {
    if (this.state.isLoading) {
      return <p>読み込み中</p>
    }
    // ↓ここで型エラーが発生！！！！！（this.state.userがundefinedかもしれないので）
    return <p>こんにちは、{this.state.user.name}さん！</p>;
  }
}
```

`this.state`を表す`State`型を定義しました。`isLoading`は`boolean`で、`user`は省略可能な（`undefined`かもしれない）オブジェクトになっています。この定義になっているのは、コンポーネントが作られた瞬間には`user`には`undefined`が入っていることを表しています。実際`user`の値が手に入るのは`loadUserData()`が終了してからなので、それまでに`user`に`undefined`を入れておくのはまあ妥当ですね。

問題は、`render()`の最後の行で型エラーが発生しているところです。根本的な原因は、プログラマは「`isLoading`が`false`ならば`user`にはちゃんとデータが入っている」ということを知っているのに、TypeScriptはそれが分かっていない点です。まあ、`State`の型宣言を見てもそんなことは書いていないので仕方ありません。

対処法としては`this.state.user!.name`のようにエラーを握りつぶすなどの手がありますが、それでは[TypeScriptに黒星をひとつ付けられる結果となります](https://qiita.com/uhyo/items/aae57ba0734e36ee846a)。

より根本的な解決は、**型をより正確に書くことでTypeScriptに分かってもらう**ことにより成し遂げられます。ただ、そのためには型定義だけでなくコードをちょっと変える必要があります。そうして達成されるのが**型ファースト**な**TypeScript way**なのです。では、今回の例をTypeScript wayで書くとどうなるでしょうか。“正解”を発表します。

```tsx:型ファーストの例
interface State {
  user:
    {
      isLoading: false;
      name: string;
    } | {
      isLoading: true;
    };
}

class App extends React.Component<{}, State> {
  state: State = {
    user: {
      isLoading: true
    }
  };

  componentDidMount() {
    loadUserData().then(user => {
      this.setState({
        user: {
          isLoading: false,
          name: user.name
        }
      });
    });
  }

  render() {
    if (this.state.user.isLoading) {
      return <p>読み込み中</p>;
    }
    // ↓型エラーが発生しない
    return <p>こんにちは、{this.state.user.name}さん！</p>;
  }
}
```

ポイントは`State`型の定義が変わっている点です。`user`プロパティの定義がunion型となっていますね。union型は「または」の意味ですから、これは「`{ isLoading: false; name: string }`型**または**`{ isLoading: true }`型」という意味です。前者は「`isLoading`の値が`false`で`name`に文字列が入ってるオブジェクト」で後者は「`isLoading`の値が`true`のオブジェクト」ですね。言い換えると、これは「`isLoading`が真偽値（`true`または`false`）で、`false`のときはさらに`name`に文字列が入っているオブジェクト」ということになります。

`isLoading`は`user`の下に入る形となって元とは変わってしまっていますが、その代わりに「`isLoading`が`false`のときのみ`name`が存在する」ということを型で表現できています。

実際の運用を見てみましょう。最初は`this.state.user`に`{ isLoading: true }`というオブジェクトが入っています。これは型定義通りですね。また、`loadUserData()`が終わったら`{ isLoading: false, name: user.name }`という別のオブジェクトを`this.state.user`に入れています。これも前述の型定義に合致していますね。

このデータを使う側である`render`関数の中身も見てみます。最初に`this.state.user.isLoading`が`true`のときは`return`する処理が入っています。これにより、TypeScriptはそれ以降で`this.state.user.isLoading`が`false`であることを理解します。

この段階では、`this.state.user.isLoading`が`false`であることが判明したことで、`this.state.user`の型がunion型の2つの候補のうち`{ isLoading: false; name: string }`の方であることが分かっています。よって、`this.state.user.name`というプロパティアクセスはエラーとなりません。技術的にはここで**union型の絞り込み**という挙動が発生しています。union型はTypeScriptからのサポートが手厚く、型の絞り込みを通してロジックの記述をサポートしてくれるのです。

このように、ロジックを漏れなく型で記述することで、TypeScriptを騙すことなく型エラーを消すことができました。ただ、その過程でコード自体を少し変える必要がありました。

`isLoading`を`user`の中に突っ込むというやり方は型ファーストな考え方をしていればそんなに変な発想ではありませんが、JavaScript脳だとあまり浮かばないのではないかと思います。このように、TypeScriptの型が持つ機能を理解しそれを活用するのが**TypeScript way**です。

ということは、TypeScriptの型がどんな機能を持っているのか知るのが非常に重要ですね。例えば以下の記事などはおすすめです（宣伝）。

- [TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)

ちなみに、別解としてはそもそも`isLoading`を廃止して`user?: { name: string }`だけにするという方法も無いわけではありません。`user`がまだ存在しない（`undefined`）な場合を読み込み中として扱うというものです。これも場合によってはありですが、コードの意味が分かりにくいので多用は避けたほうがいいかもしれません。また、この方法はすぐにはローディングが始まらない場合やローディング中のみ存在する情報（進捗率とか）がある場合に応用ができません。

## 同時に指定できないpropsがある場合

TypeScriptでReactを書く場合、propsも型を指定できます。基本的には、propsとして指定可能な属性名とその型をまとめたオブジェクトの型をpropsの型として指定しますね。

では、あるコンポーネントは`foo`と`bar`という2つのpropsがあるとしましょう。この2つのpropsは**どちらか一方しか指定してはいけない**という条件があるとします。この条件を型で表したい場合はどうすればよいでしょうか。

両方渡されても使う側が無視すればいいということにしてしまえばそれまでですが、バグの元なので折角なら型エラーで弾いてしまいたいですね。TypeScriptの型システムを最大限活かすというのには、型エラーで弾けるものは型エラーで弾いてしまおうという方向性も含まれています。

実はこの場合もunion型の出番です。「`foo`を指定する場合のpropsの型」と「`bar`を指定する場合のpropsの型」を作ってunion型で繋げましょう。

```ts
/**
 * fooを指定する場合の型
 */
interface FooProps {
  foo: number;
  bar?: undefined;
}
/**
 * barを指定する場合の型
 */
interface BarProps {
  foo?: undefined;
  bar: string;
}

type Props = FooProps | BarProps;

class MyComponent extends React.Component<Props> {}

// これはOK
const e1 = <MyComponent foo={123} />;
// これもOK
const e2 = <MyComponent bar="foobar" />;
// これは型エラー！！！！！！
const e3 = <MyComponent foo={1234} bar="hi" />;
```

`type Props = FooProps | BarProps`というようにpropsの型を定義しています。これにより`MyComponent`に渡されるpropsたちは`FooProps`か`BarProps`のどちらかに合致することが求められます。`FooProps`は`foo`を指定するときの型で、`BarProps`は`bar`を指定するときの型ですね。

例えば`FooProps`を見ると`foo`の型を`number`型とするほかに、`bar?: undefined;`という指定が入っています。これは「`bar`を指定してはいけない」、つまり「`foo`**のみ**を指定する」ことに相当します（正確には`bar={undefined}`が許可されることになりますが、この場合の挙動は`bar`を指定しなかったのと同じなので許容範囲ということにしています）。

こうすることで`Props`は「`foo`のみを指定できる型」である`FooProps`と「`bar`のみを指定できる型」である`BarProps`のどちらかという意味になり、両方指定できる可能性を排除しています。ただ、実際にやってみると分かりますがエラーメッセージが多少分かりにくいのが玉に瑕ですね。

### propsを使う側の書き方

実は、使う側もすこし工夫が必要です。単純に次のようにすると型エラーとなってしまいます。

```tsx
class MyComponent extends React.Component<Props> {
  render() {
    const { foo, bar } = this.props;
    if (foo != undefined) {
      return <p>foo is {foo}</p>;
    } else {
      // ↓ここで型エラーが発生（bar が string | undefinedなので）
      return <p>bar's length is {bar.length}</p>;
    }
  }
}
```

`Props`の定義によれば`foo`が`undefined`のときは`bar`は`undefined`ではないはずですが、TypeScriptは残念ながらこれを理解できません。この場合は次のようにすれば解決です。

```ts
class MyComponent extends React.Component<Props> {
  render() {
    const props = this.props;
    if (props.foo != undefined) {
      return <p>foo is {props.foo}</p>
    } else {
      // 型エラーが発生しない
      return <p>bar's length is {props.bar.length}</p>
    }
  }
}
```

この例では、`this.props`をさらに`foo`と`bar`に分解するのをやめて`props.foo`や`props.bar`として扱っています。こうすることで、さっきの型エラーが解消されます。

これは、あくまでunion型を持つのが`props`であることが理由です。`props`は`Props`型、すなわち`FooProps | BarProps`型を持っていますが、`props.foo`が`undefined`であることが分かった時点で`props`は`BarProps`型に決定されます。これにより、`else`節では`props.bar`が`string`型となり、`props.bar.length`でエラーが発生しません。

このような挙動はあくまで「`props`という一つの変数の型の推論」を通してサポートされます。`props`を`foo`と`bar`という2つの変数に分解した時点でこのような推論が行えなくなってしまうのです。

この例のように、union型をうまく扱いたい場合はそのunion型を持つ変数を直に扱う（今回の例では`this.props`を分解せずに扱う）ことが必要になります。これも「型のことを考えたコードを書く」の一例と言えるでしょう（TypeScriptがもっと賢ければこれは不要になるというのが引っかかりますが、この挙動をサポートするのは非現実的なので仕方ありません）。

### 常に指定すべきpropsもある場合

ここまでの内容は「`foo`と`bar`のどちらか1つのみを指定」という例で説明してきましたが、さらに複雑にして「`foo`と`bar`はどちらか1つのみ、それ以外は常に指定」みたいな場合もあります。すなわち、どちらの場合も共通で常に指定すべきpropsがある場合ですね。この場合はintersection型を使いましょう。例えば`hoge`と`fuga`は常に欲しいみたいな場合はこうします。

```ts
/**
 * 共通のpropsの型
 */
interface CommonProps {
  hoge: string;
  fuga: string;
}
/**
 * fooを指定する場合の型
 */
interface FooProps {
  foo: number;
  bar?: undefined;
}
/**
 * barを指定する場合の型
 */
interface BarProps {
  foo?: undefined;
  bar: string;
}

type Props = CommonProps & (FooProps | BarProps);
```

`&`は「かつ」という意味なので、これは`CommonProps`を満たし、かつ`(FooProps | BarProps)`も満たすということになります。とても面白いですね。

### 余談：同じことを何度も書きたくない問題を型パズルで解決

ところで、上記の解決策を見て不満に思ったことがある方がいるかもしれません。`FooProps`と`BarProps`の両方に`foo`と`bar`が登場しており、これらのプロパティ名を2回書く必要があるのが微妙ですね。何回も同じものを書くのはメンテナンス性も低いし書き間違えるかもしれません。

実はこの問題はTypeScriptのフルパワーを使うと解決できます。詳しい説明は省きますが、こうすればできます。

```ts
/**
 * propsの一覧（このうち1つのみ指定可能になる）
 */
interface AllProps {
  foo: number;
  bar: string;
  baz: boolean;
}

type Take<K extends keyof AllProps> = Pick<AllProps, K> &
  Partial<Record<Exclude<keyof AllProps, K>, undefined>>;
type DistributeTake<K> = K extends keyof AllProps ? Take<K> : never;

type Props = DistributeTake<keyof AllProps>;

class MyComponent extends React.Component<Props> {}

// これはOK
const e1 = <MyComponent foo={123} />;
// これもOK
const e2 = <MyComponent bar="foobar" />;
// これもOK
const e3 = <MyComponent baz />;
// これは型エラー！！！！！！
const e4 = <MyComponent foo={1234} bar="hi" />;
// これも型エラー！！！！！！
const e5 = <MyComponent bar="hi" baz />;
```

今回は`foo`, `bar`のほかに`baz`も増やして、この中のどれか1つしか指定できないという設定にしました。`AllProps`に全部のpropsを定義しておいてごにょごにょと型をいじるとあら不思議、目的が達成できています。

このコードを理解したいという方は筆者が書いた以下の記事で勉強しましょう（宣伝）。

- [TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)
- [TypeScriptの型初級](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f)


## クラスコンポーネントの罠とReact Hooksの話

Reactのユーザー定義コンポーネントは（今は使われていない古いやつを除けば）2種類あります。すなわち、**クラスコンポーネント**と**関数コンポーネント**です。従来クラスコンポーネントのほうが色々な機能があることから広く使われてきましたが、2019年2月に登場したReact 16.8では**Hooks**が導入され、関数コンポーネントも機能的には引けを取らなくなりました。

- 筆者が書いたHooksの記事もぜひご覧ください：[🎉React 16.8: 正式版となったReact Hooksを今さら総ざらいする](https://qiita.com/uhyo/items/246fb1f30acfeb7699da)

実際のところ、「ウチはクラスコンポーネントで回してるからそんなにHooksを使っていない」とか「学習コストがかかるのでHooksを使わないで済むならそれに越したことはない」といったことを思っている方もいるでしょう。しかし、型ファーストの観点からはやはり**Hooksを受け入れて関数コンポーネントを使うのがおすすめ**です。それは、**クラスコンポーネントよりも関数コンポーネントのほうがTypeScriptと相性がいい**からです。

つまり言いたいことは、**型のためと思って頑張ってHooksくらい身につけようぜ**ということです。ということで、旧来のクラスコンポーネントだとどの辺りが相性悪いのかを少し紹介します。一言でいえば**随所に余計な型註釈が必要**な点がいまいちです。

### stateの初期化

`state`というのは初期値が必要です。クラスコンポーネントで公式に推奨されているのは次のようにプロパティ宣言を使う方法のようです。

```ts
interface Props {
  foo: string;
}
interface State {
  bar: string | undefined;
}

class MyComponent extends React.Component<Props, State> {
  state = {
    bar: undefined
  };
  // ...
}
```

実は、この時点ですでにまずいです。上の`MyComponent`において`this.state`の型はどうなっているでしょうか。「`React.Component<Props, State>`を継承しているのだから`this.state`は当然`State`である」と思うかもしれませんが、それは間違いです。実はこの場合`this.state`の型は`state =`の右辺の式から推論されて`{ bar: undefined }`になります。これがクラスコンポーネントの罠です。この場合すぐ気づくと思いますが、`any`が紛れ込んだりすると話がとても厄介になります。

これは継承という機構を使っていることが原因で、あるクラスを継承したクラスはプロパティの型などを（より厳しくなる方向に）上書きする可能性があります。TypeScriptはそれを考慮して`state`プロパティの型を推論し直しているのです。

これを直すひとつの方法は型註釈をちゃんと付けることです。

```ts
class MyComponent extends React.Component<Props, State> {
  state: State = { // ←型註釈がついた
    bar: undefined
  };
  // ...
}
```

しかし、これは`State`と2回書いているのが良くないですね。

もうひとつの方法は、コンストラクタの中で`state`を初期化して`this.state`に代入する形にするという方法です。

```ts
class MyComponent extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      bar: undefined
    }
  }
  // ...
}
```

この場合は`state`というプロパティを宣言するのではなく既存の`this.state`に代入するという形になり、`state`の型が推論され直すという挙動は防ぐことができます。

ただ、こちらの方法にも問題点があります。コンストラクタ宣言とか`super(props);`などを書く必要がありタイプ数が多いという点[^super_props]、そして今度は`Props`を2回書かないといけないという点です。

[^super_props]: 実はこの辺には多少深い話があります。興味のある方は[Danさんのブログ](https://overreacted.io/why-do-we-write-super-props/)（[日本語訳](https://qiita.com/hand-dot/items/61a4b808f110b12e4281)）を読むのがよいでしょう。

これはクラスコンポーネントがクラスを使用していることが根本にあり、そしてTypeScriptの仕様も別に変なものではないのでクラスコンポーネントを使用している限り仕方ないと言わざるを得ません。

では根本的な解決はどうするかというと、そう、Hooksを使うのです。Hooksを使うとこうなります。

```ts
const MyComponent = (props: Props) => {
  const [bar, setBar] = useState<string | undefined>(undefined);
  // ...
};
```

この場合は`useState`の型註釈は消せないのですが（`undefined`という式から`string | undefined`型を推論するのはさすがに無理があるため）、それでも`bar`というステートの型を書くのは1箇所だけになっていますから、これでまあ十分だと思います。

### 余談：`React.FunctionComponent`型を活用しよう

上記の`MyComponent`型ですが、次のような宣言方法もあります。

```ts
const MyComponent: React.FunctionComponent<Props> = props => {
  // 中身はさっきと同じ
};
```

`MyComponent`に型註釈が付きました。その代わり、関数式の引数`props`から型註釈が消えています。これも`Props`と書くのは1回だけだしいい感じですね。なお、`React.FunctionComponent`には`React.FC`という省略形が用意されています。好きな方を使いましょう。

こちらの方法には2つの利点があります。1つは**`children`が使用できる**点、そしてもう1つは**返り値の型が宣言時に検査される**点です。

1つ目については、`React.FunctionComponent`がいい感じの定義になっているため、このように型註釈付きで宣言した関数の引数は次のように`children`が使用可能です。

```tsx
const MyComponent: React.FunctionComponent<Props> = props => {
  // ...
  return <div>{props.children}</div>;
};
```

先ほどのように`(props: Props) => { ... }`という宣言の場合はこうもいきません。`Props`の中に自分で`children`を含める必要があります。

2つ目についてですが、実は**どんな関数でも関数コンポーネントになることができるわけではありません**。特に、関数コンポーネントになるためには返り値が特定の型（`React.ReactElement | null`）である必要があります。型註釈を付けることでこの条件を満たさない関数コンポーネントの宣言をTypeScriptが弾いてくれます。

一方、型註釈がない場合は`MyComponent`はどこからどう見てもただの関数であり、（返り値に`React.ReactElement | null`のような型註釈をわざわざ書かない限りは）返り値がどんな型であってもエラーは起きません。代わりに、その変な関数を関数コンポーネントとして使用しようとした段階で初めて型エラーが発生します。これはバグに気づくのが遅れる可能性がありますからあまりよろしくないですね。

以上の2つの理由から`React.FunctionComponent`の利用がおすすめです。

### その他のクラスメソッド

話を戻しますが、クラスコンポーネントを使う場合は`state`以外にも型註釈が必要になります。例えば`componentDidUpdate`は`prevProps`, `prevState`的な引数をとりますが、これの型をちゃんと明示しないとエラーになります（`noImplicitAny`ありの場合）。理由はすでに説明した`state`の場合と同じです。

```ts
class MyComponent extends React.Component<Props, State> {
  // ↓引数に型註釈を書かないとエラー
  componentDidUpdate(prevProps, prevState) { }
}
```

TypeScriptがもうちょっと気を利かせてくれてもいいような気がしないでもないですが、こういうケースに対して合理的に気を利かせるのは意外と難しいので仕方がありません。

## 更新する`state`を動的に決めたいとき

たまにこんなコンポーネントがあります（重要な部分だけ書いているので何となく雰囲気で察してください）。

```ts
interface State {
  data1IsLoading: boolean;
  data1?: Data1;
  data2IsLoading: boolean;
  data2?: Data2;
}
class BigComponent extends React.Component<{}, State> {
  state: State = {
    data1IsLoading: true,
    data2IsLoading: true,
  };
  componentDidMount() {
    // data1とdata2の読み込みを行う
    this.load("data1", loadData1);
    this.load("data2", loadData2);
  }
  /**
   * 与えられた関数でデータを読み込んで与えられた名前のstateに突っ込む関数
   */ 
  load(stateName: string, loader: () => Promise<any>) {
    loader().then(data => {
      this.setState({
        [stateName + "IsLoading"]: false,
        [stateName]: data,
      } as any); // ←ここは型エラーで怒られるのでanyでごまかす
    });
  }
  // ...
}
```

つまり、これはデータの読み込みを複数行うでかいコンポーネントですが、「データを読み込んで`loading`を`false`にしつつ`state`を更新する」というロジックを切り出して`load`というメソッドにしています。

もちろんこれはTypeScriptとの相性がよくありません。型ファーストで書くべきではない例です。根本的にまずいのがどこかというと、`stateName + "IsLoading"`です。TypeScriptでは文字列演算の結果はどうあがいても`string`型であり（より具体的なリテラル型になったりはしない）、それを`state`のプロパティ名に使って`state`をアップデートしようとするのは無理筋です。

まあ、これに対する対処法は既に解説済みです。こうすればいいですね。

```ts
type MaybeLoading<T> =
  | {
      isLoading: false;
      data: T;
    }
  | {
      isLoading: true;
    };
interface State {
  data1: MaybeLoading<Data1>;
  data2: MaybeLoading<Data2>;
}
class BigComponent extends React.Component<{}, State> {
  state: State = {
    data1: { isLoading: true },
    data2: { isLoading: true },
  };
  componentDidMount() {
    // data1とdata2の読み込みを行う
    this.load("data1", loadData1);
    this.load("data2", loadData2);
  }
  load(stateName: string, loader: () => Promise<any>) {
    loader().then(data => {
      this.setState({
        [stateName]: {
          isLoading: false,
          data,
        },
      } as any); // ←まだ型エラーが出る
    });
  }
  // ...
}
```

これで`stateName + "IsLoading"`などというものは消えました。しかし、まだ変数`stateName`をキーに用いた`state`の更新が残っています。また、引数`stateName`や`loader`の定義が適当すぎます。実は、これくらいならTypeScriptの力でどうにかできそうです。そう、`keyof`型を使うのです。

するとまあこんな感じになります。

```ts
type DataForKey<K extends keyof State> = State[K] extends MaybeLoading<infer T> ? T : never;
class BigComponent extends React.Component<{}, State> {
  // （中略）
  load<K extends keyof State>(stateName: K, loader: () => Promise<DataForKey<K>>) {
    loader().then(data => {
      this.setState<K>({
        [stateName]: {
          isLoading: false,
          data,
        } as const
      }); // ←まだ型エラーが出る
    });
  }
}
```

`load`は`K`を何らかの`state`のプロパティ名とし、それに合致する文字列のみを引数`stateName`として受け取るようになりました。また、`loader`についてもそれと対応する型のデータを取得する関数でないと受け付けないようになりました。これならじつに安心ですね。

`this.setState`の型引数`K`を明示する必要があったりするのが多少残念です。`as const`も、`[stateName]`という動的なプロパティ名のせいでcontextual typingが効いていない（このあたりの用語について詳しく知りたい方は[TypeScriptの型推論詳説](https://qiita.com/uhyo/items/6acb7f4ee73287d5dac0)をご覧ください）のが原因で必要になっています。

結局この例で言いたかったことも、**TypeScriptが何とかできる形でコンポーネントを書こう**ということです。型に縛られるとできることが制限されると思うかもしれませんが（そしてある意味それは正しいですが）、`keyof`などのTypeScriptの機能を使うことでこれくらいは頑張れます。

### 余談：最適解はやっぱりHooks

ちなみに、上の例がやっぱりまだ何だかつらいと思った方もいるかもしれません。それは実際その通りです。そもそも`this.load("data1", loadData1);`というAPIがすでにつらいです。そして、もっとマシにする方法はあります。そう、**Hooks**を使えばいいのです。このような例では、Hooksの「ロジックをまとめられる」という特性が有効に働きます。

上のコンポーネントをHooksで書き直すとこんな感じです。非常に分かりやすくやつ書きやすい、それでいてロジックがちゃんと共通化されている完璧な解ですね。

```tsx
function useLoadedData<T>() {
    const [state, setState] = useState<MaybeLoading<T>>({ isLoading: true });

    const setData = (loader: ()=> Promise<T>) => {
        loader().then(data => {
            setState({
                isLoading: false,
                data,
            })
        })
    }
    return [state, setData] as const;
}
const BigComponent = ()=> {
    const [data1, setData1] = useLoadedData<Data1>()
    const [data2, setData2] = useLoadedData<Data2>()
    useEffect(()=> {
    // data1とdata2の読み込みを行う
    setData1(loadData1);
    setData2(loadData2);
    }, [])
  // ...
}
```

ここにたどり着くにあたっては、`MaybeLoading<T>`などの型を定義したり積極的に型引数として利用したりする必要があります。**きれいなコードを書くためなら多少ややこしい型でも臆せず使っていく**という考え方がこれを可能にしているのです。上の例の場合、型の多少難しい部分が全部`useLoadedData`の中に収まっているのもポイントが高いです。実際、`useLoadData`を使う側は簡潔な記述になっています。関数というのは、コードのロジックを閉じ込めるだけでなく型の複雑さを閉じ込めるのにも使えるのです。

## まとめ

この記事では、いくつかのReactコード例を通して型ファーストなTypeSciptコーディングの考え方を紹介しました。この記事でお伝えしたことをまとめ直すと次の通りです。

- ロジックをなるべく型に反映させることで、TypeScriptにロジックを理解してもらう。
- TypeScriptが理解できるような書き方をする。
- Hooksはいいぞ。

最後にちょっと宣伝しようと思いますが、この記事はTypeScript wayにあまり慣れていない人を対象読者にしていることもあり、TypeScriptガチ勢には物足りなかったかもしれません（TypeScriptはやばい型システムを持つことで知られていますが、この記事では余談を除けばせいぜいunion型と`keyof`型くらいしか活用していません）。React Hooksが偉すぎてそういうやばい型の出番が減っているという側面もあり、この記事で紹介したくらいの機能が使えれば大抵はいい感じにTypeScript wayでReactが書けるということでもあります。

もっとガチなTypeScriptの話がしたい方にはこのあたりの記事がおすすめです。

- [TypeScriptの型推論詳説](https://qiita.com/uhyo/items/6acb7f4ee73287d5dac0)
- [TypeScriptの型演習](https://qiita.com/uhyo/items/e4f54ef3b87afdd65546)