TypeScript には型を推論する機能があり、条件分岐の際に自動的に型を絞り込んでくれる。この仕組みを型ガード(Type Guard)と呼ぶ。
ただし万能ではなく、自動的な絞り込みが機能しないケースもある。その場合、is
を使って開発者が TypeScript に型を教えることで、解決できる。
この記事の内容は TypeScript のv3.9.5
で動作確認している。
typeof による絞り込み
型を絞り込む方法はいくつかあるが、まずはtypeof
を使った方法を紹介する。
const foo = (arg: number | string) => {
const x = arg;
if (typeof arg === "number") {
const y = arg;
} else {
const z = arg;
}
};
上記の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;
break;
}
case "string": {
const y = arg;
break;
}
default: {
const z = arg;
}
}
const a = typeof arg === "number" ? arg : null;
};
switch
文や三項演算子でも機能していることが分かる。
instanceof による絞り込み
typeof
ではなくinstanceof
を使って絞り込むことも可能。
const foo = (arg: number | number[]) => {
if (arg instanceof Array) {
const x = arg;
} else {
const y = arg;
}
};
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;
} else {
const y = arg;
}
};
arg
がa
というプロパティを持っている場合、arg
がB
である可能性が消えるため、A
に絞り込まれる。
型ガードはインラインで書かないと機能しない
型ガードは便利な機能だが、落とし穴も多い。
まず、条件式をインラインで書かないといけない。
例えば、以下のコードでは型ガードが機能する。
const foo = (arg: number | string) => {
if (typeof arg === "number") {
const x = arg;
}
};
しかし以下のコードでは、bar
においてもbaz
においても、型ガードが機能していない。
先程のfoo
の例と処理の内容は全く同じなのだが、機能しない。
const bar = (arg: number | string) => {
const isNumber = (arg: number | string) => typeof arg === "number";
if (isNumber(arg)) {
const x = arg;
}
};
const baz = (arg: number | string) => {
const isNumber = typeof arg === "number";
if (isNumber) {
const x = arg;
}
};
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 b = isNumber(arg) ? arg : null;
const c = boolean ? arg : 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;
}
};
is
を使っている関数で真偽値以外を返すと、エラーになる。
const bar = (arg: number | string) => {
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;
}
};
また、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;
}
};
オブジェクト型の絞り込み
オブジェクトに対して型の絞り込みを行う場合、気をつけなければならない点がさらに増える。
対比のためにまず、プリミティブの場合の挙動を見てみる。
以下のコードでは、arg
の型をnumber
に絞り込んだ後、関数を受け取る関数であるfoo
を実行している。
foo
に渡す関数のなかでarg
を使っているが、TypeScript はこれをnumber
だと認識してくれている。
const foo = (callback: () => void) => {
callback();
};
const bar = (arg: number | string) => {
const a = arg;
if (typeof arg === "number") {
const b = arg;
foo(() => {
const c = arg;
});
}
};
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;
if (typeof arg.x === "number") {
const b = arg.x;
foo(() => {
const c = arg.x;
const d = b;
});
const e = arg.x;
}
};
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 y = arg.x;
};
function baz() {
const z = arg.x;
}
}
};
絞り込んだ結果を保持したい場合は、先程の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;
if (xIsNumber(arg)) {
foo(() => {
const c = arg.x;
});
}
};
関数を入れ子にしても、問題なく機能する。
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 y = arg.x;
};
};
}
};