TypeScriptで型の導出を利用して楽に型付けする

\*こちらは Opt Technologiesで開催したグループ会社合同テックイベント「Opt Group Tech Day」 の発表資料になります

---

# 自己紹介

株式会社オプト シニアエンジニア @sisisin（しめにゃん）

- [GitHub](https://github.com/sisisin)
- [Twitter](https://twitter.com/_sisisin)
- フロントエンドの人だけどスクラムマスター・インフラ・サーバーサイドといろいろやります
- 今は AWS/Rails/React なプロダクトのテックリードやりつつ社内アジャイル相談窓口とか社内フロント講師（？）やってます

---

# TypeScriptで型の導出を利用して楽に型付けする

---

今日覚えて帰ってほしいこと: コード上に書いてあることは概ね型の導出が可能なので、型の二重管理がしんどいと思ったときは「もしかして導出できるかもしれない」という疑いを持ってみよう

=> 今回紹介した機能をフル活用出来なくても、「こういうこと（型の導出）ができる」ということを知っておくことで、もしかしたら今のコードベースを改善できるかもしれない？もっとTypeScriptの開発体験をよく出来るかもしれない？と疑問を持てるようになってもらえたら幸いです

---

# 例えばこういうコード書いたことありませんか？

**対応デバイスの集合を定義するときに型と値を両方書いた**

```ts
type Device = 'pc' | 'sp' | 'tablet';
const devices = ['pc', 'sp', 'tablet'];
```

---

# 例えばこういうコード書いたことありませんか？

**Storeの持つStateの型を自分で作った**

```ts
type AppState = {
  x: XState;
  y: YState;
};

const store = createStore<AppState, any, {}, {}>(
  combineReducers({
    x: xReducer,
    y: yReducer,
  }),
);
```

[playground](https://playground.type-puzzle.org?c=H4sIAAAAAAAAA41SXU%2FbMBT9K5afQAppi5AQ6WAwpEnsYUMrD2wrD45zm5r6S7bTNqv633ftJrQwHlBVOT4%2B59zje72h3FRACyqUNS6QDeEOWIBJMA4ywo0qhYafUDUcnM9IwsmWzJxRZEodHqyndDzVUx1aC%2BRxElBNLoluVAlu3MG%2FetgHJ3Sd%2BNxoH0jbeRfkyEfO56IjH5PLq71ON1IS5gnT7biXrv%2BXPu6lj%2B9Lu0A31vbnm6kmZN1rx3HX9iFwt02ivqZP97887NKn3iuLJTKy2cb%2F1VE0etO%2Fo1QrVevDZzsEK7aHyPYYl2MsTTMaPNaeiZoWG5yWskKC%2B2GDwEARevZrnB%2Fm4QHJ2twpKwUX4Ua3tAiugYzGrvPwHRtxOwe%2B8K%2Fxr43m0e0BG%2FPm6IvQ1S2TEq8o37jdO2PBhfZOiyCYFH9Z9Og5%2BxgPc9GZbrcZrcCCrkBzEUv92VDNFKT4%2BJAwPl7Vxn2j01eF0BIbl5zpWT7Mz2g08dwJu6tH71GLeVgpgaR3gF3XgWHbHZkZR76xJZskPmHWetQvoF0ZV8UAL4XdrvkxQvTA1e59cTfrmsQkboRSTX8wN7HtUiwhuVjJ2kiXyRWkok8YODoW9HQ4ujgZnp%2BMhg%2Bji2J4WpyO8rPz899JrxdpmtoqZM5DsL4YDFarVY7Is89x8APL%2BILVMOgzz40Ci0AnQH46yZFuXL2LY7zAR9oeeNYizJsyGSb6s38xLJvaf4A5EN43OD%2Bcp21KKfwcG4fZGw%2Bum2cQqjIuthMUE3KPXHdrdI0GeNoNa%2FceDjwYr6RIo%2Bg9OuRaQSd%2Fxa8ZMGf0Ab9iOmelY8osr%2BuIvSf7YNQn%2FP0DOLKU9S4FAAA%3D)

---

# その二重管理、TypeScriptなら型の導出によって解決出来ます！

---

# 先に結論だけ

```ts
const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'
```

[playground](http://www.typescriptlang.org/play/?ssl=3&ssc=1&pln=4&pc=1#code/MYewdgzgLgBAJgUwG4EtgIjAvDA2gcgAdh8AaGfCQsiqAQwCMAbBKfAXRjs1EigG4AUFACehBDAAiyNBJyjxIAGbwZ6CLjABXALYMEAJ3ZDBQA)

---

# 先に結論だけ

```ts
const store = createStore(
  combineReducers({
    x: xReducer,
    y: yReducer,
  }),
);
export type AppState = typeof store extends Store<infer S, any> ? S : never
// => AppState is { x: XState; y: YState; }
```

[playground](https://playground.type-puzzle.org?c=H4sIAAAAAAAAA41Sy27bMBD8FYKnBFBkOwgQRGleDVAgPbRFnUPaOgeKWsuM%2BQJJ2VYN%2F3uXtGQ7aQ6BDzKHM7Ozu1xTbiqgBRXKGhfImnAHLMA4GAcZ4UaVQsNPqBoOzmck4WRDps4oMqEOL1YTejnREx1aC%2BRpHFBNrohuVAnusoN%2F9bAPTug68bnRPpC28y7IkY%2Bcm6IjH5Or671ON1IS5gnT7WUvXf0vfdpLn96X9mKfGrk6bPdoosnbjo%2FWESRkVezKZVukLXbZI7A5nujjvt07a%2Fvq8WymXTlYBdCV307xk9BTcGScxWTX5IaMSUE0LNLUaEaDx6RTUdNijUtSVkhw320QGD9CL36Fa8P0PCBZmwdlpeAi3OmWFsE1kNE4bB6%2BYf%2F3M%2BBz%2Fxr%2F0mge3R4x4Zurz0JX90xK7EO%2BcfvhjAUX2gctgmBS%2FGXRo%2BfsYzzORGe62WS0AouNg%2BYilvqzppopSPHx%2FWB8bNXGc6PTvwohnINPzvQsH%2BZnNJp47oTd1qM%2FUIt5WCmBpPXj6nRguDtHpsaRr2zBxolPmLUe9XNol8ZVMcCusNtuMEaIHvi1e188TbshMYkHoVTTX8xMHLsUC0guVrI20mVyBanoMwaOjgU9HY4uTobnJ6Ph4%2BiiGJ4Wp6P87Pz8d9LredqmtgqZsxCsLwaD5XKZI%2FLic1z8wDI%2BZzUM%2Bswzo8Ai0AmQn25ypBtXb%2BMYL%2FCNtQeetQizpkyGif7id4ZlU%2FsPMAfC%2Bwb3h%2Fu0TSmFn%2BHgMHvjwXX7DEJVxsVxgmJC7pHb7htdowHedsvavocDD8YrKdIqeo8OuVXQyV%2FxawbMGX3Ar5jOWemYMovbOmLvyT4Y9Rl%2F%2FwCPBBRyJQUAAA%3D%3D)


---

# 解説

---

# Deviceの例

```ts
const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'
```

### 1行目について

- `const devices = ['pc', 'sp', 'tablet']` という配列の定義の最後に記述されている `as const`というキーワードに注目
    - この `as const` というキーワードで変数 `devices` はTuple型として推論されます
    - つまり、 `string[]` 型ではなく、 `['pc', 'sp', 'tablet']` という型になります

---

# Deviceの例

```ts
const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'
```

### ２行目について

- `type Device = ` は単にType Aliasの定義
- `typeof devices[number];` の `typeof devices` 部分で、 `device` 変数の型を取得し、 `[number]` という添字演算子にてTupleの要素一覧を取得しています
    - `['pc', 'sp', 'tablet'][0]` は `'pc'` 型
    - `typeof device[0]` は同様に `'pc'` 型
    - `typeof device[number]` は `typeof device` の全ての要素のうちのいずれかを指すので、 `'pc' | 'sp' | 'tablet'` 型
    - という流れ

---

# Deviceの例

この例ではTypeScriptの `Union Types`, `String Literal Types`, `Index types` , `const assertions` という機能を利用しています
詳しくは公式docやQiitaなどの解説記事を読んでみると良いと思います

- 公式doc: 
    - [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
    - [TypeScript 3.4
\#const assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)
- 解説記事:
    - [TypeScript3.4 の const assertion](https://qiita.com/Takepepe/items/f39c249ed31e546ecb7c)
    - [TypeScript Handbook を読む (10. Advanced Types)](https://qiita.com/murank/items/bc1776efbecbd2120d94)

---

# Storeの例

```ts
const store = createStore(
  combineReducers({
    x: xReducer,
    y: yReducer,
  }),
);
export type AppState = typeof store extends Store<infer S, any> ? S : never
```

### 1行目について

- これは単なる `store` 変数を `createStore` という関数を使って定義してるだけです
- `store` 変数はReduxの `Store<S, A>` という型が割り当てられており、このうち `S` がStateの型になります
- `createStore` は 型パラメータを４つ取る関数なのですが、ここでは指定せずにTypeScriptの推論に任せているのがポイントです

---

### 7行目について

```ts
const store = createStore(
  combineReducers({
    x: xReducer,
    y: yReducer,
  }),
);
export type AppState = typeof store extends Store<infer S, any> ? S : never
```

- `export type AppState = ` はDeviceのとき同様 Type Aliasの定義
- `typeof store extends Store<infer S, any> ? S : never` のうち、
    - `typeof store` はこれまたDeviceのとき同様に `store` 変数の型を取得しています
    - `extends Store<infer S, any> ? S : never` という部分は「 `extends` の左辺で指定した型が `Store<S, any>` 型に所属する型であれば、 `S` の型を得る、そうでなければ `never` 型を得る」　というConditional Typesという機能を利用した呪文です

---

#### Conditional Typesについて

ちょっとややこしいのでもっと簡単な例で説明します

Conditional Typesとは、 `T extends U ? X : Y` という記法で表現できる、特定の条件のときに型を分岐する事ができるという仕組みです

- `T extends U ? X : Y` は、日本語に書き下すと「TがUに所属する型ならX型を、そうでなければY型を指す」となります
- 型の三項演算子だと思ってもらえれば良くて、 `T extends U` の部分が条件式, `? X : Y` がよく利用される三項演算子と同じような振る舞いをします

---

#### Conditional Typesについて

```ts
type FooArray = string[];
type FooItem = FooArray extends string[] ? string : never;
// => FooItem's type is string!
```

上記の例では、 `FooArray` は `string[]` 型なので、 `FooArray extends string[]` は真となり、 `FooItem` は `? string : never` の左辺の `string` を指します


これが例えば `type FooArray = number[];` だったら、 `FooItem` は `never` 型になります

---

#### Conditional Typesについて

さらに `Conditional Types` の `Type inference` という機能を利用すると、型の中から型を取得できます（！？）
例えばGenericsで指定された型 `Array<T>` の `T`の部分を型としてあとから取得ができます
具体例を見ていきましょう

---

#### Conditional Typesについて

先程の例だと、 `FooArray` は `string[]` のときに `string` を得る、と明示的に書きました

```ts
type FooArray = ['foo', 'bar'];
type FooItem = FooArray extends string[] ? string : never;
// => FooItem's type is string!
```

が、 `Type inference` を利用すればこれを任意の配列型の要素を取得する、という形に出来ます
↓

```ts
type FooArray = string[];
type FooItem = FooArray extends Array<infer T> ? T : never;
```

---

#### Conditional Typesについて

```ts
type FooArray = string[];
type FooItem = FooArray extends Array<infer T> ? T : never;
```

- `FooArray extends Array<infer T> ? T : never;` について、
    -  `FooArray extends Array<infer T>` という部分が、 「 `FooArray` が `Array<T>` に所属する型なら真」という条件を表します
    -  この条件が真のとき、 `Array<infer T>` という `infer` キーワードを利用した記述によって、 `Array<T>` の `T` の型を利用できるようになります
    -  よって、 `FooArray extends Array<infer T> ? T : never` は、「 `FooArray` が `Array<T>` に所属する型の場合、 `T` の型を得る。そうでなければ `never` 型を得る」ということになります

---

# 今一度Storeの例

```ts
export type AppState = typeof store extends Store<infer S, any> ? S : never
```

- `typeof store extends Store<infer S, any> ? S : never` のうち、
    - `extends Store<infer S, any> ? S : never` という部分は 「 `extends` の左辺で指定した型が `Store<S, any>` 型に所属する型であれば、 `S` の型を得る、そうでなければ `never` 型を得る」という記述です
    - これによって、 Reduxの `createStore` 関数によって得た `Store<S, A>` 型の変数から、 `S` 型を導出することによってStateの型を得ることに成功しています

---

# Storeの例

Storeの例ではTypeScriptの `Conditional Types` という機能を利用しています
今回の解説だけではわからないことも多いと思うので、是非以下の参考文献も覗いてみてください

docや参考記事はこちら

- 公式doc: 
    - [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
- 解説記事:
    - [TypeScript 2.8 の Conditional Types について](https://qiita.com/Quramy/items/b45711789605ef9f96de)

---

# その他参考

TypeScriptの組み込み型にも今回紹介した `Index Types` や `Conditional Types` を利用した型があります
参考になると思うので、これらの実装も眺めてみると良いかもしれません

- [ReturnType](https://github.com/microsoft/TypeScript/blob/56cad36678459498580a66ccf731780b465a94f8/lib/lib.es5.d.ts#L1506)
    - `Type inference` を利用して任意の関数の返り値の型を導出する型
- [Pick](https://github.com/microsoft/TypeScript/blob/56cad36678459498580a66ccf731780b465a94f8/lib/lib.es5.d.ts#L1462)
    - `Index Types` , `Mapped Types` (今回は紹介してないですが・・・)を利用して、任意のオブジェクトから特定のフィールドだけを抽出した新しいオブジェクトの型を導出する型
- [Omit](https://github.com/microsoft/TypeScript/blob/56cad36678459498580a66ccf731780b465a94f8/lib/lib.es5.d.ts#L1486)
    - こちらは任意のオブジェクトから特定のフィールドを除いた型を導出する型
    - `Pick` と `Exclude` を組み合わせて使っているので、読み解くのは難しいかもしれませんが、こういう型の表現もあります、ということで紹介しました

---

# 最後に改めて

今日覚えて帰ってほしいこと: コード上に書いてあることは概ね型の導出が可能なので、型の二重管理がしんどいと思ったときは「もしかして導出できるかもしれない」という疑いを持ってみよう

=> 今回紹介した機能をフル活用出来なくても、「こういうこと（型の導出）ができる」ということを知っておくことで、もしかしたら今のコードベースを改善できるかもしれない？もっとTypeScriptの開発体験をよく出来るかもしれない？と疑問を持てるようになってもらえたら幸いです

---

ということでTypeScriptで型の導出を利用した楽する型付けのためのTipsでした
なるべくこういった導出のテクニックを利用して、楽して型を付けてあげて良い開発をしていきましょう

---

# Happy Hacking!