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 }; }; } };