7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

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]stringundefinedにしかなり得ないはずなのですが、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が挙げられます。
こう書くだけで、colorcolorNameのプロパティとして存在するかを確認できます。

しかし、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 colorNametrueであれば、colorColorNameKey型であるとみなす」。

よって、isColorNameKey(clor)trueとなったif文の中では、colorColorNameKey型("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を返していると想定します。
この場合のユーザー定義型ガードがどうなっているかというと、
truetrueであれば、colorColorNameKey型であるとみなす」つまり、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"));
// "緑"

参考書籍

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?