1. markey

    No comment

    markey
Changes in body
Source | HTML | Preview
@@ -1,314 +1,340 @@
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を受け付けたくないコンポーネントもあると思います。ちなみに、neverの代わりにundefinedを使っても同じことができますが、neverのほうが「絶対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の公式として用意されていませんが、自分で作ることができます。
+`id' | 'name' | 'shopId'`から`'shopId'`を引いているので、結果は`'id' | 'name'`になります。これが何に使えるかと言うと、先程紹介したPickと組み合わせると、Pickの逆、つまり特定の型から指定したキーのプロパティを**除いた**型を作れます。これをよく、`Omit`と呼んだりします。~~残念ながらOmitはTypeScriptの公式として用意されていませんが、自分で作ることができます。~~
+
+**2020年4月追記**
+
+バージョン3.5から組み込み型として`Omit type`が追加されました:tada:
+
+[Announcing TypeScript 3.5](https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/#the-omit-helper-type)
+
+なので、自作する必要はなくなったのですが、Omitがどういう仕組みになっているか知りたい人は以下の説明も読んでみてください。
### 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)
})
// userとactionsのプロパティを持った型になる
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をimportしてないのに{ 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の型を取得できます。しかし、`PropsTypeFromFC<typeof SomeComponent1> & PropsTypeFromFC<typeof SomeComponent2>`のような感じで、intersectionを挟むとかなり複雑になるため、エディタでマウスオーバーしたときにうまく中身を表示してくれないというのが悩みです。これだと素直にpropsの型をimport/exportしてもいい気がしてきました。。React.FC(FunctionComponent)をimportしてごにょごにょすればうまくできそうな気がしなくもないですが、それをするとグローバルな型にできないので本末転倒ですし。
もし、これのうまいやり方がわかる偉い人がいたら教えていただけるとありがたいです。
+**2020年4月追記**
+
+公式でReactから`ComponentProps`という型が提供されているのでそちらで取得できます:tada:
+ジェネリクスとしてReact.FCのComponentの型を渡すとそのComponentのPropsの型が取得できます!これでPropsの型をexportする必要がなくなりましたね。
+
+```tsx
+import { FC, ComponentProps } from 'react'
+
+type Props = {
+ hoge: string
+}
+
+const HogeComponent: FC<Props> = ({ hoge }) => <div>{hoge}</div>
+
+// 上のPropsと等価
+type PropsByComponentProps = ComponentProps<typeof HogeComponent>
+```
+
## 【おまけ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などを見ても、公式がステートレスで副作用のない関数型コンポーネントを推奨しているような気がします。