TypeScript: 配列の中身にtypeofで型ガード

# はじめに
　配列の内容に型検査を行い、TypeScriptで正しく型推論させる方法をご紹介します。
　以下のように使える`arrayTypeof`関数を作ります。
```typescript
(arr: any[])=>{
  if(arrayTypeof(arr, 'string')){
    arr; // string[]
  }
  if(arrayTypeof(arr, ['number','undefined'])){ // 複数の型でも使えるように
    arr; // (number|undefined)[]
  }
}
```
## 環境と凡例
　この記事にある全てのコードは[TS Playground](https://www.typescriptlang.org/play)上でTypeScript 4.7.4により実行しました。
　TS Playground上で推論された変数の型は、適宜
```typescript
hoge; // string
```
のような形でコメントとして示します。
# 結論
　結論としては、以下のコードで実装できます。
```typescript
// typeof演算子が返す値の一覧
type typenames = 'undefined'|'object'|'boolean'|'number'|'bigint'|'string'|'symbol'|'function';
// typenameから型へのマップ
type typenames2type<T extends typenames> = (
  T extends 'undefined' ? undefined :
  T extends 'object' ? object :
  T extends 'boolean' ? boolean :
  T extends 'number' ? number :
  T extends 'bigint' ? bigint :
  T extends 'string' ? string :
  T extends 'symbol' ? symbol :
  T extends 'function' ? (...args: any[])=>unknown : never
);
// 複数のtypeofをまとめて検査する型ガード関数を生成
const generateMultipleTypeof = <T extends typenames>(test_types: T[]): (v: unknown)=>v is typenames2type<T> => (
  (v): v is typenames2type<T> => (test_types as typenames[]).includes(typeof v)
)
// 汎用の配列型テスト用関数
const arrayTypeTest = <T>(obj: unknown, test: (v: any)=>v is T): obj is T[]  => (
  Array.isArray(obj) && obj.every(test)
);
// typeofに特化した配列型テスト用関数
const arrayTypeof = <T extends typenames>(v: unknown, test_types: T|T[]): v is typenames2type<T>[] => (
  arrayTypeTest(v, generateMultipleTypeof(typeof test_types === 'string' ? [test_types] : test_types))
);
```
（[TS Playground](https://www.typescriptlang.org/play?jsx=0&ts=4.7.4#code/PTAEBcE8AcFMHsBmgUe0OnegFbUDIMgV+MJoMgSBUDsGQADlByyICgo4IZYA7AQwFtYBnUAXlAHIBXOgCaxEASzqwB3AD7d4AIwBWsAMbhp3OfHgAbWAzrq6vJnNgAndXJEBzMWpmtwZsdfWtIJnesT9VI+AYA3OQgNHCMLIDSDICRDIDR6oAeDISAewyAwwyA6wyUtGH0zGwATFSwADwAKqCwAB7g9ALshRFsAHycoAAU5KCgZZXVgux8gsJiEtygAPyg-EKi4gKgAFwdXeVVNf3ySqqjExsq4AtL3at9PJo6egbjoGe6+gedR721PEYm5tugr6Zm98s9a6cbHYPlZbHR9osHisnv1HM46K4rnCXL9HgDuO5PNoPpjNNpUdD0T46H4Ah9WgA6KkMMzWVjzUD6SAAbQAugBKDiNfgAazo8AA7nQFp9YAA3czkdnBUKAcEjAA6mhEKSEASQyAPwZABYMgECGQBmDIAcS0A3+a4QDRDLFADUMgB+GQCTDIAi1PlKsA+K6ABCNyMoAo5QNZ6OYGNUALK8bTgETQXQlWhIFqlQkneq5ViNVrVRwAfUK9K6bPZDNaYoZvP5Qs5jTFoBEdVoDVYBVopWaXLaS1z2dApfL2SrNbgdc4zSTbHAado7AYFfC8azFLEym0vCErCTEcQrfZUpCYEAcjaACldCIBZRMA6EqxQBjDIBOhkAEwxbu2u937GlmBiQcNwEoDqMlRMbfN0PmCugAGggAcczzRk6EgYs23YEoWw2MsoLZToG3aToAEEzHvSAp1YNCMNaDZ2VAAAyQjQA2ClxXMSB+0cVdpXXbIkEAawZAE8nQA0ZUAdQZAH0GA9j3PS95WvOgPTvB8nwQZcuGjf5Y0reNExAgtfwA5NB3TBkSikEoswZSCO3jLtinfBCkKWETH1oF9HFzACvXEe9-UDYNQ1gMSkEXOBIxUoc4HYDg-J4ZEEQ+ZkvPTVkRVC4d2Vo4J6N4VgGC9chWjvBkcIfIomUaACQKZYsAG8lhERBWnSzDyzKlL0II4jGXQ8iJTMKjSwbZVlxargMScFxuGi0BCs6To70CUBQiZNklgAXyKkqyqwyq7xqki7wayjWmbHS4NAQLrF7Bj2s4Tqdt6giBsG4bRrAHaJs6abOmKqqMNckq7wArr4VcPqzqG9CRtCa7WSmmbHtEpdHoA5l3p6t6vneDlTqWH6zD+sBWh2qRYbMdkbtAabJuCIA)）

# コード解説
## [前提] TypeScriptの型ガード
　TypeScriptでは、typeof演算子などでオブジェクトの型を特定できる「型ガード（Type Guard）」の仕組みがあります。
　プリミティブ型であれば、以下のように`typeof`演算子による条件分岐でTypeScriptは型推論をしてくれます。
```typescript
(v: any)=>{
  if (typeof v === 'string') {
    v; // string
  }
}
```
　また、クラスであれば`instanceof`演算子で、その他の型では、「ユーザ定義型ガード」の仕組みを使うことで同様のことが実現できます。

```typescript
class HogeClass {
  constructor () {}
}
type Fuga = {
  piyo: string
};
// ユーザ定義型ガード
const isFuga = (v: unknown): v is Fuga => (
  typeof v === 'object' && v !== null && 'piyo' in v
);
// 型推論のテスト
(v: any)=>{
  if (v instanceof HogeClass) {
    v; // HogeClass
  }
  if (isFuga(v)) {
    v; // Fuga
  }
}
```

## 配列への型ガード
　配列に対する型ガードは、以下のような形で実現できます。
　（[【TypeScript】ユーザー定義型ガードと配列](https://qiita.com/sdkei/items/ef78d227309dcc16294e)に掲載されたコードから一部を抜粋して引用しました。）
```typescript
function isNamed(obj: any): obj is Named {
    return typeof obj.name === "string";
}
const obj = JSON.parse('[{ "name": "Alice" }, { "name": "Bob" }, { "name": "Charlie" }]');
if (Array.isArray(obj) && obj.every(isNamed)) {
    // ここでは obj が Named[] 型であるとみなされる。
    console.log(obj.map(named => named.name));
} else {
    console.log("`obj` is NOT array of Named.");
} // > ["Alice", "Bob", "Charlie"]
```
　`Array.isArray(obj)`で`obj`が配列であることを確認し、`obj.every(isNamed)`で各要素の型を検査する2段構えによって実現されています。

## 配列の型検査を行う汎用関数
　以上の処理を汎用処理として切り出すと、以下のようになります。
```typescript
const arrayTypeTest = <T>(v: unknown, test: (v: any)=>v is T): v is T[]  => (
  Array.isArray(v) && v.every(test)
);
const obj = JSON.parse('[{ "name": "Alice" }, { "name": "Bob" }, { "name": "Charlie" }]');
if (arrayTypeTest(obj, isNamed)) {
  obj; // Named[]
}
```
　ジェネリクスを使用していますが、引数`test`が型ガード関数であれば型から自動的に推論されます。
## typeof演算子との組み合わせ
　`arrayTypeTest`関数でシンプルに`typeof`を使用しようとした下の例はエラーとなります。
```typescript
// Error!!
// Argument of type '(v: any) => boolean' is not assignable to parameter of type '(v: any) => v is unknown'.
//   Signature '(v: any): boolean' must be a type predicate.(2345)
if (arrayTypeTest(obj, v => typeof v === 'string')) {
  obj;
}
```
　これは、`arrayTypeTest`の第2引数の戻り値が`v is string`ではなく`boolean`であるためです。下のように、明示的に戻り値の型を指定する必要があります。
```diff_typescript
- if (arrayTypeTest(obj, v => typeof v === 'string')) {
+ if (arrayTypeTest(obj, v : v is string => typeof v === 'string')) {
  obj; // string[]
}
```
## typeof型ガードの汎用関数化
　先ほどのコード中に出てきた
```typescript
v : v is string => typeof v === 'string'
```
という関数ですが、毎回書くのは大変なので、汎用関数としてファクトリ化します。
　[TypeScript Documentation](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#typeof-type-guards)によれば、typeof演算子は"string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function"の8つの値を返すため、これらの入力に対応します。
```typescript
// typeof演算子が返す値の一覧
type typenames = 'undefined'|'object'|'boolean'|'number'|'bigint'|'string'|'symbol'|'function';
// 戻り値がbooleanだと型推論が利かない
const generateTypeof = (typename: typenames): (v: unknown)=>boolean /* v is ??? */ => (
  (v) => typeof v === typename
);
//usage
const isString = generateTypeof('string'); // isString(hoge) === (typeof hoge === 'string')
```
　上記`generateTypeof`関数は、機能としては十分ですが、型ガード関数になっていません。
　続いて、型レベルプログラミングによって、文字列`typename`を推論される変数の型に変換していきます。
### 型文字列から型への変換
　typeofが返す型名から型への変換は、以下のConditional Typeで実現できます。
```typescript
// typenameから型へのマップ
type typenames2type<T extends typenames> = (
  T extends 'undefined' ? undefined :
  T extends 'object' ? object :
  T extends 'boolean' ? boolean :
  T extends 'number' ? number :
  T extends 'bigint' ? bigint :
  T extends 'string' ? string :
  T extends 'symbol' ? symbol :
  T extends 'function' ? (...args: any[])=>unknown : never
);
// usage
type t = typenames2type<'boolean'> // boolean
type u = typenames2type<'string'|'number'> // string|number
```
<details><summary>Tips: 複数文字列のUnionに対応する仕組み</summary>

　Union Distributionという性質が働いています。Union Distributionとは、型変数が条件になっているConditional Typeにおいて、Union型を与えると、それぞれの型に対するConditional TypeのUnionが返ってくる性質のことです。
```ts
type cond<T> = (
  T extends A ? _A : 
  T extends B ? _B : _OTHER
);
type t = cond<A|B>; // Union distributionにより cond<A>|cond<B>となる
```
　詳細は[TypeScriptの型初級](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f#conditional-type%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bunion-distribution)に分かりやすく説明されています。
</details>

- - -
　これを組み込むと、`generateTypeof`関数は以下の形で書けます。

```typescript
const generateTypeof = <T extends typenames>(typename: T): (v: unknown)=> v is typenames2type<T> => (
  (v): v is typenames2type<T> => v === typename
);
//usage
const isString = generateTypeof('string'); // (v: unknown) => v is string
(obj: any) => {
  if(isString(obj)) obj; // string
}
```
### 複数の型への対応
　`generateTypeof`関数は、1つの型文字列だけを引数に取る関数でした。そのため、`number|undefined`のような型をチェックしたいときは、`generateTypeof`を2回呼び出してorで条件分岐する必要があります。面倒なので、型文字列の配列でもチェックできるよう`generateMultipleTypeof`関数を作りましょう。
```typescript
const generateMultipleTypeof = <T extends typenames>(test_types: T[]): (v: unknown)=>v is typenames2type<T> => (
  (v): v is typenames2type<T> => (test_types as typenames[]).includes(typeof v)
)
```
　実装はシンプルで、`typeof v`の値が`test_types`配列に含まれているかどうか`Array.prototype.includes`関数で検査するだけです。
## `arrayTypeTest`と`generateMultipleTypeof`の合成
　最後に、以上で実装した2つの関数を合わせて、配列の各要素にtypeofによる型検査を適用する関数`arrayTypeof`関数を作成します。
```typescript
const arrayTypeof = <T extends typenames>(v: unknown, test_types: T|T[]): v is typenames2type<T>[] => (
  arrayTypeTest(v, generateMultipleTypeof(
    typeof test_types === 'string' ? [test_types] : test_types // test_typesを一律で配列にする
  ))
);
```

# おわりに
　最後までお読みいただき、ありがとうございました。
　実装してみて、ジェネリクスを使った型推論や`typenames2type`の実装など、小技の勉強になりました。お暇なTypeScriptビギナーの方は冒頭のコードを見て動作を追ってみてください。また、この記事の主題ではありませんが、`generateMultipleTypeof`関数も個人的にはよく使っています。よろしければそちらもお使いください。
　感想やコードの問題点、改善点などありましたら、コメント頂ければ幸いです。