はじめに
TypeScriptを触り始めて数ヶ月が経ったのですが、「ユーザー定義型ガード」というものを使う(使わざるをえない)機会に遭遇したので、得た知見をまとめておきます。
プロパティへのアクセスについて
変数を使って、プロパティにアクセスするときは、以下のようにブラケット記法(Obj[key])を使います。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
const color = "green";
console.log(colorName[color]);
// "緑"
上記の例では、変数colorに"green"を代入して、colorName[color]でプロパティにアクセスしています。
では次に、仮引数を使って、プロパティにアクセスしてみます。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
const getColorName = (color: string): string | undefined => {
return colorName[color]; // colorName[color]でコンパイルエラー!
};
console.log(getColorName("green"));
// "緑"
上記のように書くと、先ほどとは異なりcolorName[color]の部分で、以下のようなコンパイルエラーが出てしまいます。
型 'string' の式を使用して型 '{ red: string; green: string; blue: string; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
型 'string' のパラメーターを持つインデックス シグネチャが型 '{ red: string; green: string; blue: string; }' に見つかりませんでした。ts(7053)
これは、colorNameのプロパティとして指定できるのは"red"or"green"or"blue"だけであるが、仮引数colorは文字列であればどんな値も入る可能性があるため、colorName[color]の型は保証できない、と言っています。
(colorName[color]はstringかundefinedにしかなり得ないはずなのですが、TypeScriptはこのことを理解できず、「暗黙的に 'any' 型になります。」と言っている?)
コンパイルエラーを回避する
一番シンプルな方法は、仮引数colorの型を、colorNameのプロパティに限定することです。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"
const getColorName = (color: ColorNameKey): string | undefined => {
return colorName[color];
};
しかし、colorには任意の文字列が入ることを考慮する場合もあるかと思います。
その場合のシンプルな方法は、colorName[color]を実行する前に、colorが"red"or"green"or"blue"であるということを保証することです。
とりあえず、if (color === "red" || color === "green" || color === "blue")でやってみます。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
const getColorName = (color: string): string | undefined => {
if (color === "red" || color === "green" || color === "blue") {
// ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
return colorName[color];
}
return undefined;
};
この方法でも、コンパイルエラーを回避できました。
しかしこの書き方だと、プロパティの数が増えたとき、ifの条件式も長くなってしまいます。
そこで考えられる代替案としては、color in colorNameが挙げられます。
こう書くだけで、colorがcolorNameのプロパティとして存在するかを確認できます。
しかし、color in colorNameを使った場合、先ほどと同様のコンパイルエラーになってしまいます。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
const getColorName = (color: string): string | undefined => {
if (color in colorName) {
// ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
// が、TypeScriptは上記を理解できていない
return colorName[color]; // colorName[color]でコンパイルエラー!
}
return undefined;
};
型 'string' の式を使用して型 '{ red: string; green: string; blue: string; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
型 'string' のパラメーターを持つインデックス シグネチャが型 '{ red: string; green: string; blue: string; }' に見つかりませんでした。ts(7053)
ifブロック内では、colorが"red"or"green"or"blue"であることを保証できているのですが、おそらくTypeScriptがこのことを理解しておらず、コンパイルエラーになっていると思われます。
ユーザー定義型ガードを使う
TypeScriptが理解できないのであれば為す術はないと思うかもしれませんが、「ユーザー定義型ガード」を使うとTypeScriptに理解させることができます。
まず、colorが"red"or"green"or"blue"であるかを判別しているcolor in colorNameを関数として切り出します。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
// 判別部分を関数として切り出す
const isColorNameKey = (color: string): boolean => {
return color in colorName;
};
const getColorName = (color: string): string | undefined => {
if (isColorNameKey(color)) {
// ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
// が、TypeScriptは上記を理解できていない
return colorName[color]; // colorName[color]でコンパイルエラー!
}
return undefined;
};
そうしたら、切り抜いた関数の、返り値の型を指定する部分にkey is ColorNameKeyと書きます。
こうすることで、ユーザー定義型ガードとなります。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"
// ユーザー定義型ガード
const isColorNameKey = (color: string): color is ColorNameKey => {
return color in colorName;
};
const getColorName = (color: string): string | undefined => {
if (isColorNameKey(color)) {
// ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
// ユーザー定義型ガードによりTypeScriptも、"red" | "green" | "blue" であるとみなしている
return colorName[color];
}
return undefined;
};
上記のユーザー定義型ガードを日本語訳すると以下のような感じになります。
「color in colorNameがtrueであれば、colorはColorNameKey型であるとみなす」。
よって、isColorNameKey(clor)がtrueとなったif文の中では、colorはColorNameKey型("red"or"green"or"blue")であるとTypeScriptはみなしているため、colorName[color]でコンパイルエラーが出なくなります。
注意点
ユーザー定義型ガードは、型の実態に関わらず、型の情報をプログラマが上書きすることもできてしまうので、注意が必要です。
例としてまず、getColorName関数が、必ず文字列を返してほしくなったとして、以下のように修正します。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"
// ユーザー定義型ガード
const isColorNameKey = (color: string): color is ColorNameKey => {
return color in colorName;
};
const getColorName = (color: string): string => {
if (isColorNameKey(color)) {
// ユーザー定義型ガードのロジックにより、
// 変数colorの型は "red" | "green" | "blue" であることが保証されている。
// ユーザー定義型ガードによりTypeScriptも、
// "red" | "green" | "blue" であるとみなしているため、コンパイルエラーにならない。
return colorName[color];
}
return "定義されていません";
};
console.log(getColorName("green"));
// "緑"
console.log(getColorName("black"));
// "定義されていません"
上記のように修正したのち、ユーザー定義型ガードに誤りがある場合を考えてみます。
今回は、誤ってisColorNameKey()が常にtrueを返していると想定します。
この場合のユーザー定義型ガードがどうなっているかというと、
「trueがtrueであれば、colorはColorNameKey型であるとみなす」つまり、colorが何であってもColorNameKey型だとみなされてしまいます。
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"
// ユーザー定義型ガード(実際は型ガードできていない)
const isColorNameKey = (color: string): color is ColorNameKey => {
return true;
};
const getColorName = (color: string): string => {
if (isColorNameKey(color)) {
// ユーザー定義型ガードのロジックの誤りにより、
// 変数colorの型は "red" | "green" | "blue" であることが保証されていない。
// しかし、ユーザー定義型ガードによりTypeScriptは、
// "red" | "green" | "blue" であるとみなしているため、コンパイルエラーにならない。
return colorName[color];
}
return "定義されていません";
};
console.log(getColorName("green"));
// "緑"
console.log(getColorName("black"));
// undefined
//(文字列以外が返却されてしまうが、コンパイルエラーは出ていない、、、)
この場合、colorに"red"or"green"or"blue"以外が指定されてもcolorName[color]が実行されます。しかし、TypeScriptは、if文の中でcolorは"red"or"green"or"blue"であるとみなしているため、コンパイルエラーになりません。
なので、colorName("black")が想定外の挙動をするにも関わらず、コンパイルエラーが出ないため、バグに気づきにくくなってしまいます。
ユーザー定義型ガードを使用する際には、ロジックに誤りがないか慎重に確認しましょう。
おまけ
以下のように、anyを使うことでも、一応コンパイルエラーは回避できます。
(anyは可能な限り避けるべきですが)
const colorName = {
red: "赤",
green: "緑",
blue: "青"
};
const getColorName = (color: string): string | undefined => {
return (colorName as any)[color];
};
console.log(getColorName("green"));
// "緑"
参考書籍