TypeScript5.3で追加される(予定の)switch (true)による型の絞り込み

## はじめに
[TypeScript5.3のBeta版の発表](https://devblogs.microsoft.com/typescript/announcing-typescript-5-3-beta/)がありました。
そこでは様々な新機能の追加がありましたが、この記事では`switch(true)`における型の絞り込みについて紹介します。

## 型の絞り込み
TypeScriptでは制御フローに応じた型の絞り込みを行えます。`string | number | undefined`を型として持つような変数が絞り込みが可能な制御フローのスコープ内では`string | number | undefined`から特定の`string`、`number`、`undefined`を型として持つ変数として扱われます。
```ts
function narrowing(x: string | number | undefined) {
  if (typeof x === 'string') {
    // xの型はstringとして扱われる
    return x;
  }
  if (x !== undefined) {
    // xの型はnumberとして扱われる
    return x;
  }
  // xの型はundefinedとして扱われる
  return x;
}
```

## switchによる制御フロー
TypeScript5.2以前では`switch`文による型の絞り込みは行えません。
```ts
function narrowing(x: string | number | undefined) {
  switch (true) {
    case (typeof x === 'string'):
      // xの型はstring | number | undefined
      x
      break
    case (x !== undefined):
      // xの型はstring | number | undefined
      x
      break
    default:
      // xの型はstring | number | undefined
      x    
  }
}
```
TypeScript5.3以降では`switch`文によって以下のように変数の方を絞り込むことが可能です([Playground](https://www.typescriptlang.org/play?ts=5.3.0-dev.20231005#code/GYVwdgxgLglg9mABGAhgJzXA7jMBzACgA8AuRAZyjVz0QB9kQBbAIwFM17FwATN4XGx4BKRAG8AUIgo4oEABaICVEG1GTp0iCnJslUAJ4AHNnGCIiiALw3EAckrV8d4SSmbpRdx5Zo2KAGtvRG1dJUsAQltefkERNw9PYOlffyCPPmAUEAAbKATEy013AF8JMqA))。
```ts
function narrowing(x: string | number | undefined) {
  switch (true) {
    case (typeof x === 'string'):
      // xの型はstring
      x
      break
    case (x !== undefined):
      // xの型はundefined
      x
      break
    default:
      // xの型はnumber
      x    
  }
}
```
ただ、`switch`文で内で文脈を読んで絞り込みが行われるようになったわけではないです。以下のような場合は絞り込みは行われないことに注意してください。
```ts
function narrowing(x: string | number | undefined) {
  switch (typeof x) {
    case ('string'):
      // xの型はstring | number | undefined
      x
      break
    case ('number'):
      // xの型はstring | number | undefined
      x
      break
    default:
      // xの型はstring | number | undefined
      x    
  }
}

function narrowing(x: string | number | undefined) {
  switch (false) {
    case (typeof x === 'string'):
      // xの型はstring | number | undefined
      x
      break
    case (typeof x === 'number'):
      // xの型はstring | number | undefined
      x
      break
    default:
      // xの型はstring | number | undefined
      x    
  }
}
```
つまり、`switch (true)`だった時に`case`の式に合わせてスコープ内で絞り込みが行われるということです。
なぜそうなっているか、どのように実装されているかは[こちらのPR](https://github.com/microsoft/TypeScript/pull/53681/files#diff-d9ab6589e714c71e657f601cf30ff51dfc607fc98419bf72e04f6b0fa92cc4b8R27301-R27307)を参照してください。

## おわりに
この記事ではTypeScript5.3の新機能として追加される`switch(true)`における型の絞り込みについて紹介しました。
以下のようにオブジェクトの一部によってユニオンを絞り込んで利用したいケースではif文の羅列よりもswitchで一連の分岐を表記すると見やすくなることもあるので選択肢の１つとして利用できるようになると良いと思いました。
```ts
type Result<T> = {
  success: true;
  contents: T
} | {
  success: false;
  contents: Error
}

function unwrap<T>(x: Result<T>) {
  switch (true) {
    case x.success:
      return x.contents;
    default:
      throw x.contents;
  }
}

type Loadable<T> = {
  state: "hasValue";
  contents: T;
} | {
  state: "loading";
  contents: Promise<T>;
} | {
  state: "hasError";
  contents: unknown;
};

async function getContents<T>(loadable: Loadable<T>) {
  switch (true) {
    case (loadable.state === 'hasValue'):
      return loadable.contents;
    case (loadable.state === 'loading'):
      return await loadable.contents;
    case (loadable.state === 'hasError'):
      throw loadable.contents;
  }
}
```