はじめに
この投稿は、TypeScript学習者が自身の理解のために書いています。型ガード関数を書いたとき、「関数本体では絞り込まれないのに、呼び出し側のifブロック内では絞り込まれる」点と「includesに渡すとコンパイルエラーになる」点を、整理していきます。
判別可能なユニオン型でプロパティを比較してifブロック内で型を絞り込む方法もありますが、今回は型ガード関数を使う場合を扱います。URLクエリなど実行時にはstring | nullとして入ってくる値を、アプリ内では文字列リテラルのユニオン型で扱う例です。
const SORT_OPTIONS = ["newest", "oldest"] as const;
type SortOption = (typeof SORT_OPTIONS)[number];
function isSortOption(value: string | null): value is SortOption {
return SORT_OPTIONS.includes(value as SortOption);
}
function parseSort(raw: string | null): SortOption {
if (isSortOption(raw)) {
return raw;
}
return "newest";
}
parseSortのifブロック内では、引数rawの型がSortOption型に絞り込まれます。一方、型ガード関数isSortOptionの関数本体では、引数valueの型はstring | nullのままです。includesの呼び出しでは型アサーションが必要になります。
1. 型述語のtrueが効くのは呼び出し側のif
value is SortOptionという戻り値の注釈は型述語です。関数がtrueを返したとき、呼び出し側のifブロック内では、関数に渡した変数の型がSortOption型に絞り込まれます。
const SORT_OPTIONS = ["newest", "oldest"] as const;
type SortOption = (typeof SORT_OPTIONS)[number];
function isSortOption(value: string | null): value is SortOption {
// このブロック内の value は string | null のまま
return SORT_OPTIONS.includes(value as SortOption);
}
declare const raw: string | null;
if (isSortOption(raw)) {
const narrowed: SortOption = raw;
}
上のifブロック内では、変数rawの型がSortOption型に絞り込まれます。判別可能なユニオン型におけるstate.status === "success"のような判別子の比較とは異なり、型ガード関数がtrueを返したことが絞り込みの条件になります。
型述語による絞り込みは、定義した関数本体には伝播しません。isSortOptionの関数本体では、引数valueの型はstring | nullのままなので、return valueのようにそのままSortOption型として返すことはできません。実行時チェックを書いたうえでtrue/falseを返し、型の絞り込みは呼び出し側のifブロック内で行われます。
2. as constとincludesが受け付ける型
次に、関数本体でas SortOptionが必要になる理由です。配列リテラルにas constを付けると、型はstring[]には広がらずreadonly ["newest", "oldest"]というreadonlyタプル型として推論されます。SortOption((typeof SORT_OPTIONS)[number])は"newest" | "oldest"です。ここから先は、その結果としてincludesが受け付ける型がどう変わるかを見ます。
const SORT_OPTIONS = ["newest", "oldest"] as const;
type SortOption = (typeof SORT_OPTIONS)[number];
declare const value: string | null;
// SORT_OPTIONS.includes(value);
// エラー:Argument of type 'string | null' is not assignable to parameter of type '"newest" | "oldest"'
SORT_OPTIONSの型がreadonlyタプル型であるため、includesが受け付ける型はSortOption("newest" | "oldest")に狭くなり、string | nullはそのまま渡せません。
TypeScriptがここまで狭くするのは、SORT_OPTIONS.includes("invalid_value")のように、タプルの要素に含まれない文字列との比較をコンパイル時に検知するためです。一方、URLクエリのように広い型から値が許可リストに含まれるかを調べる場合は、同じincludesでもコンパイルエラーになります。
function isSortOption(value: string | null): value is SortOption {
return SORT_OPTIONS.includes(value as SortOption);
}
実行時のincludesは文字列の一致を調べるだけです。型チェックでは、渡す値の型(string | null)とincludesが受け付けるSortOptionが一致しません。型ガード関数内では引数の型がまだstring | nullのままなので、as SortOptionで型を合わせる対応ができます。呼び出し側のifブロック内ではasは不要で、変数はSortOption型として扱えます。
まとめ
-
value is Tという型述語は、呼び出し側のifがtrueになったブロック内で、関数に渡した変数の型をTに絞り込む。型ガード関数内では、その引数型は変わらない。 - 配列リテラルに
as constを付けるとreadonly ["newest", "oldest"]のようなreadonlyタプル型として推論され、そのincludesが受け付ける型は狭い。 -
string | nullのような広い型をincludesに渡すとコンパイルエラーになる。型ガード関数内ではasでSortOptionに合わせ、呼び出し側のifブロック内ではasなしでSortOption型として使える。