TypeScript の構文の中でも、屈指のググラビリティの低さを誇る is
と in
を自分なりにまとめてみました。
"is"
is
は TypeScript の型推論を補強する user-defined type guard(ユーザー定義型ガード)
で使われます。
unknown 型や、any 型、Union 型の型の絞り込みを行えます。
使用例
例えば、unknown 型を引数に受け取る関数で、もし引数の型が string 型だったら文字列の長さを出力するという処理があるとします。
その場合は、以下のように型を絞り込む必要があります。
const example = (foo: unknown) => {
if (typeof foo === "string") {
console.log(foo.length); // fooはstringとして推論される
}
};
この一つの関数ならば良いのですが、他にも string 型への絞り込みが必要な関数があり、typeof foo === "string"
の部分を isString
関数に抽出して汎用的に使いまわせるようにしたいとします。
一見上手くいきそうですが、typeof での型の絞り込みは関数スコープで完結してしまうので、isString
が true な場合でも、まだ foo は unknown 型として推論され型の絞り込みが行えません。
const isString = (test: unknown): boolean => {
return typeof test === "string";
};
const example = (foo: unknown) => {
if (isString(foo)) {
console.log(foo.length); // Error fooはまだunknownとして推論される
}
};
このような時が is
の出番です。
is
を使うと isString
の結果が true の場合は引数で受け取った変数の型は、string 型であるとコンパイラに教えることができます。
const isString = (test: unknown): test is string => {
return typeof test === "string";
};
const example = (foo: unknown) => {
if (isString(foo)) {
console.log(foo.length); // fooはstringとして推論される
}
};
また、is
はオブジェクト型の型の絞り込みにも使えます。
例えば Fish か Bard の Union 型の型をどちらかの型に絞り込みたい時はif((fishOrBard as Bard).fly() !== undefined){}
で判定を行えそうです。
しかし、この判定ではまだ TypeScript のコンパイラは fishOrBard
を Bard 型と推論してはくれません。
type Bard = {
fly: () => {
/* Do something */
};
};
type Fish = {
swim: () => {
/* Do something */
};
};
const example = (fishOrBard: Fish | Bard) => {
if ((fishOrBard as Bard).fly() !== undefined) {
console.log(fishOrBard.fly); // Fish | Bard 型として推論され Fishはfly()を持たないので type error
} else {
console.log(fishOrBard.swim); // Fish | Bard 型として推論され Bardはswim()を持たないので type error
}
};
そのような時に (fishOrBard as Bard).fly() !== undefined
を関数に切り出し、is
で型を指定するとコンパイラは型を推論できるようになります。
const isBard = (test: Fish | Bard): test is Bard => {
return (test as Bard).fly() !== undefined;
};
const example = (fishOrBard: Fish | Bard) => {
if (isBard(fishOrBard)) {
console.log(fishOrBard.fly); // Bard型として推論される
} else {
console.log(fishOrBard.swim); // Fish型として推論される
}
};
注意点
is
自体への型推論は効かないので注意です。間違った指定をしてしまうとコンパイルは通っても実行時エラーになります。
const isString = (test: unknown): test is string => {
return typeof test === "number";
};
const example = (foo: unknown) => {
if (isString(foo)) {
console.log(foo.length); // 型エラーは出ないが、fooはnumber型なので実行時Errorが発生する
}
};
(その挙動の解決策としてこちらの記事で便利なライブラリが紹介されていました。)
"in"
in
は コンテキストによって 2 つの意味を持つ構文です。
一つは JS にもあるオブジェクトが特定のプロパティを持つか判定するもの。型の絞り込みに使えます。
もう一つは keyof
構文と組み合わせて mapped type の定義に使えます。
使用例
型の絞り込み
前述の is
で オブジェクトの型判定に(fishOrBard as Bard).fly() !== undefined)
を使いましたが、こちらは in
で代替することができます
in
を使えば判定処理を関数に切り出し is
で type predicate を記述する必要もありません。
const example = (fishOrBard: Fish | Bard) => {
// fishOrBardは'fly'というプロパティを持っているかどうかの判定
if ("fly" in fishOrBard) {
console.log(fishOrBard.fly); // Bard型として推論される
} else {
console.log(fishOrBard.swim); // Fish型として推論される
}
};
mapped type での利用
keyof
構文と組み合わせて mapped type で新しい型を定義できます。
keyof
プロパティ名の Union 型を取り出し in
で反復処理するイメージですかね。
type Dog = {
name: string;
run: () => void;
};
type PartialDog = {
[P in keyof Dog]?: Dog[P];
};
// PartialDogは以下の型になる
// { name?: string, run?: () => void}
終わりに
以上 TypeScript の is
と in
構文の説明でした。
分かってみると何かと便利に使えそうですね。
自分自信まだまだ勉強中なので誤った記述などあれば、コメントや修正リクエストで教えて頂けると嬉しいです!!