Open7

『サバイバルTypeScriptの逆引き』の逆引きページ用のネタストック

ピン留めされたアイテム
suinsuin

このスクラップはTypeScript入門者向けウェブ書籍『サバイバルTypeScript』の逆引きTipsに追加しても良さそうなネタをストックするところです。

詳しい検証がなされていないものや正確性に乏しいものもあるかもしれないのでご注意ください。

suinsuin

オブジェクトの値から、そのキーのユニオン型を計算する

↓のようなオブジェクト定数ありきのコードで、

const status = {
  ok: 200,
  notFound: 404,
  internalServerError: 500,
} as const;

ここから、

type StatusName = "ok" | "notFound" | "internalServerError";

のような型を計算で導き出したい。

やりかた: keyoftypeofでやる

type StatusName = keyof typeof status;
suinsuin

オブジェクトの値から、そのプロパティの値のユニオン型を計算する

const status = {
  ok: 200,
  notFound: 404,
  internalServerError: 500,
} as const;

こういう定数が定義されていて、ここから

type StatusCode = 200 | 404 | 500;

のようなnumberリテラル型のユニオン型を導き出したい。

やりかた: typeof + keyof + インデックスシグネチャ

type StatusCode = typeof status[keyof typeof status];
suinsuin

union型の分岐漏れをコンパイラにチェックさせる方法

↓のようなユニオン型がコーディングされていて

type Feelings = "happy" | "sad";

それを絵文字にマッピングする関数があるとして↓

function toEmoji(feelings: Feelings): void {
  switch (feelings) {
    case "happy":
      console.log("😄");
      break;
    case "sad":
      console.log("😢");
      break;
  }
}

toEmoji('happy'); //=> 😄
toEmoji('sad'); //=> 😢

で、Feelingsに型を追加したとき、開発者がtoEmojiの存在を知らないか、あまり気にしてなかった場合、どうなるか?

toEmojiの挙動が未定義なものになってしまう。

// 改修後のコード
type Feelings = "happy" | "sad" | "not_so_good";

toEmoji('not_so_good');  // 何も出力されない……

これを、コンパイル時に気づけるようにする小技がある。

それが、網羅チェック(exhaustive checks)パターン。

function toEmoji(feelings: Feelings): void {
  switch (feelings) {
    case "happy":
      console.log("😄");
      break;
    case "sad":
      console.log("😢");
      break;
    default: // デフォルトケースを追加し
      const exhaustiveCheck: never = feelings; // never型に変数をアサインするのがポイント!
  }
}

この網羅チェックコードを追加した状態で、Feelingsを拡張すると……

コンパイラが問題のヒントをくれるようになる。

exhaustiveCheckがunused variableになって気持ち悪い。もしくは、noUnusedLocalsコンパイルオプションで禁じ手になっている場合は↓のような例外を投げるアイディアもある。

class InvalidStateError extends Error {
  constructor(value: never, message?: string) {
    super(message);
  }
}

type Feelings = "happy" | "sad" | "not_so_good";

function toEmoji(feelings: Feelings): void {
  switch (feelings) {
    case "happy":
      console.log("😄");
      break;
    case "sad":
      console.log("😢");
      break;
    default:
      throw new InvalidStateError(feelings, `Unexpected state: ${feelings}`);
  }
}

これなら、TypeScriptの仕様のために書いた謎の変数exhaustiveCheckが存在する奇抜さもないし、JavaScript的な文脈でも「あ、用心深いコードだな」と思われるくらい。そして、コンパイルエラーを無視してしまうような大ミスをしても、実行時に例外になるので安心。

おまけ: eslintで網羅チェックする方法もある: typescript-eslint/switch-exhaustiveness-check.md at master · typescript-eslint/typescript-eslint