TypeScript には型を推論する機能があり、条件分岐の際に自動的に型を絞り込んでくれる。この仕組みを型ガード(Type Guard)と呼ぶ。
ただし万能ではなく、自動的な絞り込みが機能しないケースもある。その場合、isを使って開発者が TypeScript に型を教えることで、解決できる。
この記事の内容は TypeScript のv3.9.5で動作確認している。
typeof による絞り込み
型を絞り込む方法はいくつかあるが、まずはtypeofを使った方法を紹介する。
const foo = (arg: number | string) => { const x = arg; // const x: string | number if (typeof arg === "number") { const y = arg; // const y: number } else { const z = arg; // const z: string } };
上記のfooは、引数としてnumberかstringを受け取る。
xの時点では、どちらなのかは判断できない。
だがif文でargのtypeofをチェックしており、ifブロックのなかではargはnumberであることが確定している。そして、TypeScript はそれを認識しており、yはnumberだと自動的に推論してくれる。
そして、elseブロックのなかではargはnumberではないことが確定しているため、型が絞り込まれ、stringであると推論される。
型の絞り込みは、if文以外の条件分岐でも機能する。
const foo = (arg: number | string) => { switch (typeof arg) { case "number": { const x = arg; // const x: number break; } case "string": { const y = arg; // const y: string break; } default: { const z = arg; // const z: never } } const a = typeof arg === "number" ? arg : null; // const a: number | null };
switch文や三項演算子でも機能していることが分かる。
instanceof による絞り込み
typeofではなくinstanceofを使って絞り込むことも可能。
const foo = (arg: number | number[]) => { if (arg instanceof Array) { const x = arg; // const x: number[] } else { const y = arg; // const y: number } };
argはnumberかnumber[]のいずかだが、Arrayのインスタンスである場合、numberである可能性が排除されるため、number[]に絞り込まれる。
in による絞り込み
inでプロパティの有無を調べることでも、絞り込みが行われる。
type A = { a: number } type B = { b: number } const foo = (arg: A | B) => { if ('a' in arg) { const x = arg; // const x: A } else { const y = arg; // const y: B } };
argがaというプロパティを持っている場合、argがBである可能性が消えるため、Aに絞り込まれる。
型ガードはインラインで書かないと機能しない
型ガードは便利な機能だが、落とし穴も多い。
まず、条件式をインラインで書かないといけない。
例えば、以下のコードでは型ガードが機能する。
const foo = (arg: number | string) => { if (typeof arg === "number") { const x = arg; // const x: number } };
しかし以下のコードでは、barにおいてもbazにおいても、型ガードが機能していない。
先程のfooの例と処理の内容は全く同じなのだが、機能しない。
const bar = (arg: number | string) => { const isNumber = (arg: number | string) => typeof arg === "number"; if (isNumber(arg)) { const x = arg; // const x: string | number } }; const baz = (arg: number | string) => { const isNumber = typeof arg === "number"; if (isNumber) { const x = arg; // const x: string | number } };
if文以外も同じで、インラインで書いたa以外では型ガードが機能していない。
const foo = (arg: number | string) => { const isNumber = (arg: number | string) => typeof arg === "number"; const boolean = typeof arg === "number" const a = typeof arg === "number" ? arg : null; // const a: number | null const b = isNumber(arg) ? arg : null; // const b: string | number | null const c = boolean ? arg : null; // const c: string | number | null };
ユーザー定義型ガード
isを使うことで、条件式を切り出すことが可能になる。
isは、開発者が TypeScript に対して型を教えるための機能で、「ユーザー定義型ガード」と呼ばれる。
関数の返り値を引数 is Tとアノテートすると、「その関数がtrueを返す場合は引数はTであり、falseを返す場合はTではない」と TypeScript に指示することになる。
以下のisNumberは、返り値をarg is numberとアノテートしているため、trueを返した場合はargはnumberであると見做される。
そのため、ifブロックのなかではargがnumberに絞り込まれている。
const bar = (arg: number | string) => { const isNumber = (arg: number | string): arg is number => typeof arg === "number"; if (isNumber(arg)) { const x = arg; // const x: number } };
isを使っている関数で真偽値以外を返すと、エラーになる。
const bar = (arg: number | string) => { // Type '1' is not assignable to type 'boolean'.ts(2322) const isNumber = (arg: number | string): arg is number => 1; };
この機能を使うことで、型ガードを関数として切り出すことが可能になるし、複雑な型ガードを実装することも可能になる。
しかしisもまた万能ではなく、注意点がいくつかある。
まず、ユーザー定義型ガードは条件式のなかで使う必要がある。
以下のコードでは、isNumberの結果をbooleanに代入しそれを条件式で使っているが、そうすると型ガードが機能しなくなってしまう。
const bar = (arg: number | string) => { const isNumber = (arg: number | string): arg is number => typeof arg === "number"; const boolean = isNumber(arg); if (boolean) { const x = arg; // const x: string | number } };
また、isを使ったアノテートが実装と矛盾していたとしても、TypeScript はエラーを出さないので、開発者が注意を払わなければならない。
以下のisNumberは、arg is numberとするべき箇所を、arg is stringと書いてしまっている。
だがエラーにはならず、TypeScript はその指示を愚直に信じてしまう。
その結果、xは実際にはnumberなのだが、TypeScript はそれをstringだと判断してしまう。
const bar = (arg: number | string) => { const isNumber = (arg: number | string): arg is string => typeof arg === "number"; if (isNumber(arg)) { const x = arg; // const x: string } };
オブジェクト型の絞り込み
オブジェクトに対して型の絞り込みを行う場合、気をつけなければならない点がさらに増える。
対比のためにまず、プリミティブの場合の挙動を見てみる。
以下のコードでは、argの型をnumberに絞り込んだ後、関数を受け取る関数であるfooを実行している。
fooに渡す関数のなかでargを使っているが、TypeScript はこれをnumberだと認識してくれている。
const foo = (callback: () => void) => { callback(); }; const bar = (arg: number | string) => { const a = arg; // const a: string | number if (typeof arg === "number") { const b = arg; // const b: number foo(() => { const c = arg; // const c: number }); } };
ifブロックのなかではargが絞り込まれているのだから、ごく自然な挙動だと思う。
しかし、argの型をオブジェクトにすると、直感に反する挙動になる。
以下のコードでは、argはxというプロパティを持つオブジェクトであり、xの型はnumber | stringである。
そして、typeofを使った絞り込みを行っている。
type SomeObject = { x: number | string; }; const foo = (callback: () => void) => { callback(); }; const bar = (arg: SomeObject) => { const a = arg.x; // const a: string | number if (typeof arg.x === "number") { const b = arg.x; // const b: number foo(() => { const c = arg.x; // const c: string | number const d = b; // const d: number }); const e = arg.x; // const e: number } };
bを見ると、絞り込みが機能していることが分かる。
eもnumberになっているため、ifブロックのなかではarg.xはnumberと推論されているはずである。
しかしcを見ると、なぜか絞り込みが行われず、string | numberになってしまっている。
実はオブジェクトの場合、新しい関数のスコープが作られると、そのなかでは型ガードによる推論結果が失われてしまう。
type SomeObject = { x: number | string; }; const bar = (arg: SomeObject) => { if (typeof arg.x === "number") { const x = arg.x; // const x: number () => { const y = arg.x; // const y: string | number }; function baz() { const z = arg.x; // const z: string | number } } };
絞り込んだ結果を保持したい場合は、先程のdのように、推論された値を変数(b)に入れておきそれを代入する必要がある。
あるいは、ユーザー定義型ガードを使うことでも解決できる。
type SomeObject = { x: number | string; }; const foo = (callback: () => void) => { callback(); }; const xIsNumber = (arg: SomeObject): arg is { x: number } => { return typeof arg.x === "number"; }; const bar = (arg: SomeObject) => { const a = arg.x; // const a: string | number if (xIsNumber(arg)) { foo(() => { const c = arg.x; // const c: number }); } };
関数を入れ子にしても、問題なく機能する。
type SomeObject = { x: number | string; }; const xIsNumber = (arg: SomeObject): arg is { x: number } => { return typeof arg.x === "number"; }; const bar = (arg: SomeObject) => { if (xIsNumber(arg)) { () => { const x = arg.x; // const x: number () => { const y = arg.x; // const y: number }; }; } };