LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-03

はじめに

 配列の内容に型検査を行い、TypeScriptで正しく型推論させる方法をご紹介します。
 以下のように使えるarrayTypeof関数を作ります。

(arr: any[])=>{
  if(arrayTypeof(arr, 'string')){
    arr; // string[]
  }
  if(arrayTypeof(arr, ['number','undefined'])){ // 複数の型でも使えるように
    arr; // (number|undefined)[]
  }
}

環境と凡例

 この記事にある全てのコードはTS Playground上でTypeScript 4.7.4により実行しました。
 TS Playground上で推論された変数の型は、適宜

hoge; // string

のような形でコメントとして示します。

結論

 結論としては、以下のコードで実装できます。

// 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

コード解説

[前提] TypeScriptの型ガード

 TypeScriptでは、typeof演算子などでオブジェクトの型を特定できる「型ガード(Type Guard)」の仕組みがあります。
 プリミティブ型であれば、以下のようにtypeof演算子による条件分岐でTypeScriptは型推論をしてくれます。

(v: any)=>{
  if (typeof v === 'string') {
    v; // string
  }
}

 また、クラスであればinstanceof演算子で、その他の型では、「ユーザ定義型ガード」の仕組みを使うことで同様のことが実現できます。

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】ユーザー定義型ガードと配列に掲載されたコードから一部を抜粋して引用しました。)

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段構えによって実現されています。

配列の型検査を行う汎用関数

 以上の処理を汎用処理として切り出すと、以下のようになります。

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を使用しようとした下の例はエラーとなります。

// 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であるためです。下のように、明示的に戻り値の型を指定する必要があります。

- if (arrayTypeTest(obj, v => typeof v === 'string')) {
+ if (arrayTypeTest(obj, v : v is string => typeof v === 'string')) {
  obj; // string[]
}

typeof型ガードの汎用関数化

 先ほどのコード中に出てきた

v : v is string => typeof v === 'string'

という関数ですが、毎回書くのは大変なので、汎用関数としてファクトリ化します。
 TypeScript Documentationによれば、typeof演算子は"string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function"の8つの値を返すため、これらの入力に対応します。

// 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で実現できます。

// 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
Tips: 複数文字列のUnionに対応する仕組み

 Union Distributionという性質が働いています。Union Distributionとは、型変数が条件になっているConditional Typeにおいて、Union型を与えると、それぞれの型に対するConditional TypeのUnionが返ってくる性質のことです。

type cond<T> = (
  T extends A ? _A : 
  T extends B ? _B : _OTHER
);
type t = cond<A|B>; // Union distributionにより cond<A>|cond<B>となる

 詳細はTypeScriptの型初級に分かりやすく説明されています。


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

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関数を作りましょう。

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関数で検査するだけです。

arrayTypeTestgenerateMultipleTypeofの合成

 最後に、以上で実装した2つの関数を合わせて、配列の各要素にtypeofによる型検査を適用する関数arrayTypeof関数を作成します。

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関数も個人的にはよく使っています。よろしければそちらもお使いください。
 感想やコードの問題点、改善点などありましたら、コメント頂ければ幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0