はじめに
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"));
// "緑"
参考書籍