TypeScript:判別可能なユニオンの分割代入をしやすくするFillKeysユーティリティー型についての紹介

TypeScriptでの開発において、[判別可能なユニオン型]は非常に便利ですが、オブジェクトの分割代入を行いたいときにはいくつかの問題が発生します。この記事では、その問題点と`FillKeys`ユーティリティー型を使った解決策について解説します。

[判別可能なユニオン型]: https://typescriptbook.jp/reference/values-types-variables/discriminated-union

## 問題点

以下のサンプルコードで示すように、`Success`型と`Failure`型のユニオン型を分割代入しようとすると、コンパイルエラーが発生してしまいます。

```typescript
type Success = {
  type: "success";
  value: number;
};
type Failure = {
  type: "failure";
  error: Error;
};
declare const result: Success | Failure;
const { type, value, error } = result; // 分割代入できない
// Property 'value' does not exist on type 'Success | Failure'.(2339)
// Property 'error' does not exist on type 'Success | Failure'.(2339)
```

## 解決策

`FillKeys`ユーティリティー型を使うと上記の問題を解決できます。まず、`FillKeys`を定義します。

```typescript
type FillKeys<T> = (
  (T extends T ? keyof T : never) extends infer AllKeys
    ? T extends T
      ? { [K in keyof T]: T[K] } & {
          [K in AllKeys extends keyof T
            ? never
            : AllKeys extends string
            ? AllKeys
            : never]?: undefined;
        }
      : never
    : never
) extends infer T
  ? { [K in keyof T]: T[K] }
  : never;
```

<details><summary>FillKeysの細かい解説</summary>

```typescript
type FillKeys<T> = (
  (T extends T ? keyof T : never) extends infer AllKeys
  // ↑ユニオン型の全要素のキーを洗い出し、AllKeysに代入する
    ? T extends T // 各要素ごとに処理したいので、union distributionを発動するための術式
       // ↓これはもともとのオブジェクトと、その要素が持たないキーとundefinedのペアからなるオブジェクトをマージする部分
      ? { [K in keyof T]: T[K] } & {
          [K in AllKeys extends keyof T
            ? never
            : AllKeys extends string
            ? AllKeys
            : never]?: undefined;
        }
      : never
    : never
  // ↓この部分は上で計算されたインターセクション型をフラットにする部分。
  //  {...} & {...} | {...} & {...}  | {...} & {...} のような見ずらい計算結果を
  //  {...} | {...} | {...} のようにきれいにする。この部分は無くても動くので、お好みで。
) extends infer T
  ? { [K in keyof T]: T[K] }
  : never;
```

</details>

そして、以下のように`result`型を`FillKeys<Success | Failure>`に変更し、分割代入を行います。

```typescript
declare const result: FillKeys<Success | Failure>;
const { type, value, error } = result; // 分割代入できる！コンパイルエラーもない
```

`FillKeys`ユーティリティ型を使って`Success | Failure`を変換すると、以下のような型になります。

```typescript
{
    type: "success";
    value: number;
    error?: undefined;
} | {
    type: "failure";
    error: Error;
    value?: undefined;
}
```



この型では、`Success`と`Failure`の両方の型が考慮され、それぞれの型に存在しないプロパティが`undefined`としてオプショナルで追加されます。これにより、分割代入を行う際にコンパイルエラーが発生しなくなります。

具体的には、`Success`型には`error`プロパティが存在しないため、`error?: undefined;`が追加されます。同様に、`Failure`型には`value`プロパティが存在しないため、`value?: undefined;`が追加されます。

これにより、判別可能なユニオン型の分割代入が容易になり、[型の絞り込み](https://typescriptbook.jp/reference/statements/control-flow-analysis-and-type-guard)も機能します。

```typescript
declare const result: FillKeys<Success | Failure>;
const { type, value, error } = result;
// 型の絞り込み
if (type === "success") {
  value * 100; // ちゃんとvalueがnumber型と解釈されています
} else {
  error.message; // ちゃんとerrorがError型と解釈されています
}
```

この投稿で紹介した`FillKeys`はTypeScript Playgroundでも試せるようになっています↓

https://www.typescriptlang.org/play?target=99&jsx=4#code/C4TwDgpgBAygrgYwRAziqBeKBvAUFKUSALigCIVFk0yBufKANwEMAbOCUgOzgFsAjCACd6AX3q4i0AGLMAluyHQseAlNJkAZvMUQ6DYUID2Q0gFEhxkbnG5cXZr1RhmyKIAe1QNEMgEIZAJgw4GABMIBFZmJSgEIy4UYCglSlZgUngkVHQAHyhZBTglegIomLjsQnAIABomNg4qwxMoUUx41Dgk2igAek6oQDAlQCclQGO5QFNFQHMGQFkGQCsGQBEGQDICWZs7KWyFVgBpCBAUAB4AFQA+ZoAKBiPdqAgAD2AILkD0c4B+KABrTaNNKHPuCEZhAEoLtdbvcoHIuJphFAAIKsdabFAMAjPc5XG53B5IghQZ6lADaazBXFe70+uwAuqRdgTyY0oAAyALY5nYglEmFwjZbIHo0FvEAfL5YlnY55cX7CYUiqCkWHw7lokHoWJCcEAcylIuecq5iOlLJ+fyE5MepDgdwgmnBEECBWloilhslzKdQlwgMVGKJkKEQuROCgbPBJIFZMpXxpjQYrokDicKBcbkAYgyAGIZAOoM0z8TKgwVC4WgRViLUSyRWnIR21S1Ey2R0eQg+zthZKZUgVRY7EqF0sDSaWASbWAHW6fSGY3GnkAgP92AgjwDR6oA7BkAer6AKIZAD3xgH8GQBBDIB9K0AvwGATQZPAw5J8jssMJfyJQ0jRAapsR2OHbZz0AHqPBhNCCsFDQB8EPU1jMiOH5fosQA

## まとめ

判別可能なユニオン型を扱う際に、分割代入が難しい問題に直面することがあります。しかし、`FillKeys`ユーティリティー型を使うことで、この問題を解決し、分割代入が容易になります。