TypeScript は JavaScript を元に静的型付けを拡張した言語です。その特性上、引数や返り値、処理中において値の扱い方を間違いにくいです。静的型付けとは言いますが、実際には動的に値を検査する必要もしばしば出てきます。型ガードはそういった動的検査と静的型付けをつないでくれる TypeScript の機能です。
TypeScript: JavaScript With Syntax For Types.
型ガード – TypeScript Deep Dive 日本語版#ユーザー定義のType Guard
型ガードは次の様に使います。
// https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard
// 上記から引用
function doSomething(x: number | string) {
if (typeof x === 'string') { // ブロック内では、TypeScript は `x` が文字列でなければならないことを認識しています。
console.log(x.subtr(1)); // エラー, 'subtr' メソッドは `string` 型の中にありません
console.log(x.substr(1)); // OK
}
x.substr(1); // エラー: x` が `string` である保証はないため substr が呼べません
}
上記例ではtypeof x === 'string'で x が string 型であることを TypeScript に示しています。この様に型ガードではある値がどの様な型なのか TypeScript に教えられます。この型ガードですが、任意のオブジェクトの型にも使えます。しかし扱いにくさもあります。例えば次です。
type User = {
name: string;
};
const isUser = (v: unknown): v is User =>
// v がオブジェクトであることを検証
typeof v === 'object' && v != null
// v にプロパティ v があることを検証
&& 'name' in v
// v.name が String 型であることを検証しようとする
// object 型に name というプロパティはないと怒られる
// Property 'name' does not exist on type 'object'.
&& typeof v.name === 'string';
isUser({name: '浜松太郎'});
一見、正しいですし、問題ないのですが TypeScript はこのままでは検査を通してくれません(設定を緩くすれば通るはずでもあります)。これを力技で解決するのであれば as, any, @ts-ignore が使えます。それぞれ次の様に使えます。
type User = {
name: string;
};
/** as */
const isUser = (v: unknown): v is User =>
typeof v === 'object' && v != null
&& 'name' in v
// v が User 型であるとTypeScriptに伝えることで name の参照を許す
&& typeof (v as User).name === 'string';
/** any */
// v が何でもありの型と示すことで解決する
const isUser = (v: any): v is User =>
typeof v === 'object' && v != null
&& 'name' in v
&& typeof v.name === 'string';
/** @ts-ignore */
const isUser = (v: unknown): v is User =>
typeof v === 'object' && v != null
&& 'name' in v
// @ts-ignore TypeScript のエラーを無視する
&& typeof v.name === 'string';
これらでも一応解決できますが、@ts-ignore は真に誤った時にエラーを出力してくれないのは困りますし、any や as は厳格な TypeScript ルールのもとでコードを作る場合に使えません。厳格な TypeScript で @ts-ignore も使わない場合、次の様にして解決できます。
// まずどのキーからどの様な値でも格納できるオブジェクトであることを示す型ガード関数を作ります
const isObjectHasAnyProperty = (v: unknown): v is { [v: string | number | symbol]: unknown } =>
v != null && typeof v === 'object';
// この型ガード関数で識別されるオブジェクトは
// 自由なプロパティを設定できるオブジェクトであるとTypeScriptが認識してくれます
const obj = {};
// 型ガードを通る前にオブジェクトに動的プロパティを生やすと次の様に怒られます
// TS2339: Property 'hoge' does not exist on type '{}'.
// obj.hoge = 1;
if (isObjectHasAnyProperty(obj)) {
// 型ガードを通れば怒られません
obj.hoge = 1;
}
// 上記の関数による型ガードを用いてどの様なプロパティでも持てるオブジェクトであると示すと
// 各プロパティについてのチェックで TypeScript はエラーを吐かなくなります
type User = {
name: string;
};
const isUser = (v: unknown): v is User => isObjectHasAnyProperty(v) && 'name' in v && typeof v.name === 'string';
isUser({ name: '浜松太郎' });
上記コードの様にどの様なプロパティも持ちうる型であると一度 TypeScript に示しておくことで、その後の型ガードの構築が楽になります。また、もし IDE などでプロパティ名を自動補完したい場合は次の様なコードを作るのも手です。
type User = {
name: string;
};
// // Partial<T> とすることで T のプロパティを持ちうるオブジェクトであると示せます
// // const isPartialObject = <T>(v: unknown): v is Partial<T> => v != null && typeof v === 'object';
function isPartialObject<T>(v: unknown): v is Partial<T> {
return v != null && typeof v === 'object';
}
const isUser = (v: unknown): v is User => isPartialObject<User>(v) && typeof v.name === 'string';
isUser({ name: '浜松太郎' });
こちらは isPartialObject 関数を通ると渡した型と同じ名のプロパティを持ちうるオブジェクトであると TypeScript に示せます。