はじめに
配列の内容に型検査を行い、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))
);
コード解説
[前提] 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
関数で検査するだけです。
arrayTypeTest
とgenerateMultipleTypeof
の合成
最後に、以上で実装した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
関数も個人的にはよく使っています。よろしければそちらもお使いください。
感想やコードの問題点、改善点などありましたら、コメント頂ければ幸いです。