1. markey

    Posted

    markey
Changes in title
+React開発において便利なTypeScriptの型まとめ
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,313 @@
+React開発において個人的に便利だなーと思っているTypeScriptの型をだだーっとまとめてみました。私自身もまだまだTypeScript修行中の身ですので、新たに気づいたものがあったら随時追記していきます。みなさんも「こういう使い方できるぜ!」みたいなのがあったら、ぜひ教えていただければと思います。
+
+## 対象とする読者
+
+- 最近ReactにTypeScriptを導入し始めた人
+- ReactにTypeScriptを導入してそこそこ経つけど、いまいち使いこなせてる気がしない人
+
+TypeScriptにあまり詳しくない人でもわかるように説明しているつもりではありますが、以下の記事がTypeScriptの入門用に素晴らしいので、そちらを先に読むとスムーズに読み進められると思います。
+[TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)
+
+## Partial
+
+React開発においてよく定義する型としてコンポーネントのpropsの型があると思います。例えばButtonコンポーネントみたいなのがあったとして、文字や色をpropsで受け取るとします。ただし、これらは必須ではなくて、propsが渡されなかった場合は別に用意したデフォルトの値を使います。
+
+```typescript
+// titleおよびcolorはstringもしくはundefinedとなる
+type ButtonProps = {
+ title?: string
+ color?: string
+}
+```
+
+TypeScriptでは`?`をつけることでそのプロパティがオプションだと示すことができます。ひとつひとつのプロパティに?を付けていくのでもいいですが、全部のプロパティをオプションにしたいときは`Partial`が便利です。
+
+```typescript
+// 先程の例と同じ型になる
+type ButtonProps = Partial<{
+ title: string
+ color: string
+}>
+```
+
+これで先程の例と同じものが表現できます。今回はプロパティが2つなのでうまみを感じにくいですが、プロパティが多い場合だとひとつひとつに?をつけるのが面倒なのと、パっと見て全部のプロパティがオプションなんだなとわかるのは後者のほうだと思います。
+
+## never
+
+TypeScriptを勉強し始めた当初、いまいち使いどころわからなかったのが、`never`です。本当に使う機会がなくて、いらない子扱いしてた(ごめんよ)のですが、コンポーネントのchildren propsで使うと便利です。以下のようなコンポーネントを作ってみました。ヘッダー用のコンポーネントで、具体的な中身はchildrenで渡すようにします。
+
+```tsx
+import { FC } from 'react'
+
+type HeaderProps = { name: string }
+
+const Header: FC<HeaderProps> = ({ name, children }) => {
+ return (
+ <div>
+ <p>{name}さんこんにちは</p>
+ <div>{children}</div>
+ </div>
+ )
+}
+
+const Root: FC = () => {
+ return <Header name="名無し">ようこそ</Header>
+}
+```
+
+なお、この記事ではReact.Componentをextendsしたクラス型コンポーネントではなく、React.FCを使った関数型コンポーネントを使用します。[^1]React.FCを使うと型引数で渡した型と共に`children?: React.ReactNode`というpropsを受け取ることができます。?がついているのでオプションになってますね。つまり、デフォルトではchildrenを渡しても渡さなくてもどっちでもいいわけですが、これはちょっと曖昧な気がします。上記のようなコンポーネントだと必ずchildrenを受け取りたいですし、逆にchildrenを受け付けたくないコンポーネントもあると思います。
+
+### children?: neverでchildrenを拒否する
+
+そのようなときにpropsの型定義でchildrenを上書きすると、childrenを受け取るか受け取らないか明示することができます。`HeaderProps`の型定義を以下のように変えてみます。
+
+```typescript
+import { ReactNode } from 'react'
+
+// childrenを必ず受け取る(?を取り除く)
+type HeaderProps = {
+ name: string
+ children: ReactNode
+}
+
+// childrenを拒否する(never型にする)
+type HeaderProps = {
+ name: string
+ children?: never
+}
+```
+
+エディタ上で編集すればわかりますが`children: ReactNode`にした状態でHeaderにchildrenを渡さないとコンパイルエラーになり、逆に`children?: never`にした状態でchildrenを渡すとこれまたコンパイルエラーになります。このようにして、propsの型定義によってそのコンポーネントがchildrenを受け取るか受け取らないかを明示することができます。
+
+## Pick
+
+続いて、コンポーネントのpropsのみならず、幅広い場所で使えるPickです。名前のイメージ通り、特定の型の中から指定したキーのプロパティのみを抽出する型です。型引数の1つ目に抽出元の型、2つめに抽出するプロパティのキー(union型(`|`)で複数指定可)を指定します。
+
+```typescript
+type ShopItem = {
+ id: string
+ name: string
+ shopId: string
+}
+
+// ShopItem型からidとnameのプロパティを抽出した型を生成
+type Item = Pick<ShopItem, 'id' | 'name'>
+
+// ↑と一緒
+type Item = {
+ id: string
+ name: string
+}
+```
+
+既存の型から新しい型を作りたい際に便利です。
+
+## Exclude
+
+続いて、`Exclude`ですが、こちらはPick型の逆かと思いきや少し使い方が違い、型引数の1つ目のunion型から2つ目の型(Pickと同様union型で複数指定可)を除いた型となります。ちょっと言葉ではわかりづらいので例を見てみましょう。
+
+```typescript
+// 結果は'id' | 'name'
+type ItemKey = Exclude<'id' | 'name' | 'shopId', 'shopId'>
+```
+
+`id' | 'name' | 'shopId'`から`'shopId'`を引いているので、結果は`'id' | 'name'`になります。これが何に使えるかと言うと、先程紹介したPickと組み合わせると、Pickの逆、つまり特定の型から指定したキーのプロパティを**除いた**型を作れます。これをよく、Omitと呼んだりします。残念ながらOmitはTypeScriptの公式として用意されていませんが、自分で作ることができます。
+
+### Omit型を自作する
+
+Omit型の実装は以下になります。ちょっと複雑なので分解していきましょう。Tは抽出元の型、Kは除きたいプロパティのキー(union型で複数指定可)だと思ってください。
+
+```typescript
+type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
+```
+
+まずは`keyof`というキーワードですが、`keyof T`でTの全てのプロパティのキーのunion型になります。例えば、以下のようになります。
+
+```typescript
+type Item = {
+ id: string
+ name: string
+}
+
+// 結果は、'id' | 'name'
+type ItemKey = keyof Item
+```
+
+Item型のプロパティはidとnameなので、`'id' | 'name'`になりますね。つまり`K extends keyof T`というのは、KはTの全てのプロパティのキーのunion型の一部ということになります。TからKのプロパティを除きたいので当然(TにKのプロパティがなければKを除く意味がありません)ですね。続いて、
+
+```typescript
+Exclude<keyof T, K>
+```
+
+この部分ですが、今までの知識を使うと「Tの全てのプロパティのキーのunion型からKを除いた型」となります。言い換えると「TからKを除いた全てのプロパティのキーのunion型」になります。これをPickの型引数の2つ目に指定すると、TからKを除いた型になるわけです。
+
+```typescript
+type ShopItem = {
+ id: string
+ name: string
+ shopId: string
+}
+
+type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
+
+// この2つは全く同じ型
+type Item = Pick<ShopItem, 'id' | 'name'>
+type Item = Omit<ShopItem, 'shopId'>
+```
+
+## intersection
+
+ここまでPick、Exclude、Omitと紹介してきましたが、実は個人的にほとんど使っていません。というのも既存の型を再利用したいときは、`intersection`をよく使っています。これは`A & B`とすると、AとBをマージした型を生成できるというものです。
+
+```typescript
+type Item = {
+ id: string
+ name: string
+}
+
+type Shop = {
+ shopId: string
+}
+
+// Item型とShop型をマージ
+type ShopItem = Item & Shop
+
+// ↑と一緒
+type ShopItem = {
+ id: string
+ name: string
+ shopId: string
+}
+```
+
+PickやOmitを特定の型から指定のプロパティを除く、つまり引き算とすると、intersectionは型を組み合わせる足し算です。どちらを使うかはケースバイケースや好みにもよると思いますが、個人的には大きな型をくずして小さな型を作るよりかは、小さな型を組み合わせて大きな型を作るという後者の方がメンテナンス性が高いのではないかと思っているので、主にintersectionを使っています。
+
+## ReturnType
+
+続いて、`ReturnType`ですが、こちらは型引数に関数型を指定するとその戻り値の型を取得できるものです。例を見てみましょう。
+
+```typescript
+const plus = (x: number, y: number): number => {
+ return x + y
+}
+
+// number型になる
+type PlusFunctionReturnType = ReturnType<typeof plus>
+```
+
+`plus`は数字を2つ引数で受け取って、両者を足したものを返すという単純な関数です。`typeof`でその変数の型を取得できます。つまり`typeof plus`とは`(x: number, y: number) => number`型となります。それをReturnTypeの型引数に指定してるので、結果としてnumber型が取得できるわけです。
+
+これをどこで使えるかと言うと、一つの例としてReduxとコンポーネントを接続する箇所です。Presentational Componentに渡すpropsとして、mapStatoToPropsとmapDispatchToPropsを定義すると思いますが、そちらで使うと便利です。
+
+```typescript
+// SomeContainer.tsx
+// AppStateとかDipatchとかconnectとかもろもろimportしている想定です
+import SomeComponent from '../presentational/SomeComponent'
+
+const mapStateToProps = ({ user }: AppState) => ({
+ user
+})
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ // actionをまとめたものだと思ってください
+ actions: new ActionDispather(dispatch)
+})
+
+export type Props = ReturnType<typeof mapStateToProps> &
+ ReturnType<typeof mapDispatchToProps>
+
+export default connect(mapStateToProps, mapDispatchToProps)(SomeComponent)
+
+
+// SomeComponent.tsx
+import { FC } from 'react'
+import { Props } from '../container/SomeContainer'
+
+const SomeComponent: FC<Props> = ({ user, actions }) => {
+ // 省略
+}
+```
+
+注目していただきたいのは、`ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>`の箇所です。よくよく考えてみると、SomeComponentに渡されるPropsの型は、mapStatoToPropsの戻り値とmapDispatchToPropsの戻り値を合体させたものなわけですから、それぞれのReturnTypeのintersection型にすればいいわけです。このPropsの型を愚直に定義するとこうなります。
+
+```typescript
+type Props = {
+ user: User
+ actions: ActionDispathcher
+}
+```
+
+しかし、この場合だと、SomeComponentに渡すpropsを新たに増やすことを考えてみてください。mapStatoToPropsやmapDispatchToPropsに修正を加えるとともに、上記のPropsの型定義も修正しなければなりません。一方で、ReturnTypeを使う場合だと、動的に型を生成するため、mapStatoToPropsやmapDispatchToPropsに修正を加えると自動的にPropsの型定義にも反映されます!これは気持ちよくないですか?
+
+## プロパティアクセス
+
+最後にプロパティアクセスを紹介します。プロパティアクセスとは`T[K]`と書くと、TのKというキーのプロパティの型を取得できるものです。例を見てみましょう。
+
+```typescript
+type User = {
+ id: number
+ name: string
+}
+
+// number型になる
+type IdType = User['id']
+```
+
+User型のプロパティidはnumber型なので、number型が取得できます。これをどこで使えるかと言うと、クラス型コンポーネントのpropsの型を取得することができます。
+
+```typescript
+// Header.tsx
+import { Component } from 'react'
+
+type Props = { name: string }
+
+class Header extends Component<Props> {
+ // 省略
+}
+
+export default Header
+
+
+// OtherComponent.tsx
+import Header from './Header'
+
+// Header.tsxからPropsをexportしてないのに{ name: string }が取れた!
+type HeaderProps = Header['props']
+```
+
+当たり前のことですが、クラス型コンポーネントはClassなわけですから'props'というキーにアクセスできますし、そのコンポーネントのpropsの型が取得できました。TypeScriptを使っていく中での一つの悩みとして、やたらと型をimport/exportして記述量が増えてしまうというのがあるのですが、この方法を使うとコンポーネントをexportするだけで済み、propsの型をimport/exportする必要はありません。ただ、既に述べたとおり、コンポーネントをClassで書くのはできるだけやめて、関数で書くようにしていますが、その場合だとこの方法は使えません。だって、関数ですからね。
+
+### 関数型コンポーネントのpropsの型を取得する(したかった)
+
+そのため、関数型コンポーネントでもpropsの型を取得する方法を考えたのですが、私のレベルだとこれが限界でした。
+
+```typescript:index.d.ts
+type FirstArgumentType<T extends Function> = T extends (...args: infer A) => any ? A[0] : never
+type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
+declare type PropsTypeFromFC<T extends Function> = Omit<FirstArgumentType<T>, 'children'>
+```
+
+ざっくり説明すると`FirstArgumentType`で、ある関数の最初の引数の型を取得します。`T extends Function`なので、関数型コンポーネント以外の普通の関数も受け付けてしまいますが。。`Omit`は先程説明したものですね。`FirstArgumentType`はchildrenも含まれてしまうので、Omitでそれを除いてます。一応これで関数型コンポーネントでもpropsの型を取得できるのですが、いかんせん複数の手順を踏んでるため、エディタでマウスオーバーしたときにうまく中身を表示してくれないというのが悩みです。これだと素直にpropsの型をimport/exportしてもいい気がしてきました。。React.FC(FunctionComponent)をimportしてごにょごにょすればうまくできそうな気がしなくもないですが、それをするとグローバルな型にできないので本末転倒ですし。
+
+もし、これのうまいやり方がわかる偉い人がいたら教えていただけるとありがたいです。
+
+## 【おまけ1】type vs interface
+
+TypeScriptを使っていると誰しも一度は思うことだと思いますが、typeとinterfaceって結局どっちを使えばいいんだっけ?という疑問です。以下の記事が詳しいですが、実は両者はほとんど違いがなく、書き方は違えどだいだいどっちも同じことができます。
+
+[TypeScriptのInterfaceとType aliasの比較](https://qiita.com/tkrkt/items/d01b96363e58a7df830e)
+
+なので、好みやプロジェクトごとのスタイルになると思うのですが、個人的にはtypeを推しています。理由としては、これまでに紹介した、型を加工するような処理を行うと、typeを必ず使わなくてはいけないシーンがあるからです。対して、interfaceを必ず使わなくてはいけないシーンには個人的にあまり出会ったことがなく、どのみちtypeを使わなければいけないのだから、最初から全部typeで統一してしまおうというモチベーションです。ただ、このへんは私もあまり詳しくないので、もし「interface使ったほうがいいよ!」という情報がありましたら教えていただけるとありがたいです。
+
+## 【おまけ2】TypeScriptコードの不吉な匂い
+
+かの有名なリファクタリングの「コードの不吉な匂い」ですが、もしTypeScriptを書いていて以下のように感じることがあれば、それは改善のサインかも知れません。
+
+- 型定義を変更するときにやたらと変更箇所が多い
+- やたらと型のimport/exportが多い
+
+TypeScriptでReactを開発した感想としては、型の恩恵を受けられるのはすごく良いと思いつつも、どうしても記述量が増えてしまうというのが少し不満でした。ここまでで紹介したような動的な型を駆使すれば、ある程度そのつらみを減らすことができると思います。
+
+この記事を読んで、みなさんが、つらくない楽しいReact + TypeScriptライフを送ってくれるようになれば幸いです。
+
+[^1]: 最近導入されたReact Hooksなどを見ても、公式がステートレスで副作用のない関数型コンポーネントを推奨しているような気がします。