指定キーを取り除いたオブジェクトを再作成する

# はじめに

TypeScriptでオブジェクトから任意のキーを取り除いたオブジェクトを非破壊的に作成する関数を実装しました。
また、上記関数から型も上手く定義するようにしました。

やりたいことをPythonで書くと下記のようになります。

```py
def filtered_dict(target: dict[str, Any], remove_keys: set[str]) -> dict[str, Any]:
  return {
    k: v
    for k, v in target.items()
    if k not in remove_keys
  }
```

# 実装

anyも入っているし型アサーションを用いていますが実行結果は期待する通りになるので許してください。

```ts
const strictEntries = <T extends Record<string, any>>(data: T): [keyof T, T[keyof T]][] => {
  return Object.entries(data);
};

const filteredObject = <T extends Record<string, any>, K extends keyof T>(originalObject: T, removeKeys: K[]) =>
  Object.fromEntries(strictEntries(originalObject).filter(([key, _]) => !removeKeys.includes(key as K))) as Omit<T, K>;
```

## 使用方法

```ts
// OK
filteredObject(
  {
    ten: 10,
    aaa: 'aaa',
    fibonacci: [1, 1, 2, 3, 5, 8, 13],
  },
  ['ten', 'aaa']
);  // { fibonacci: [1, 1, 2, 3, 5, 8, 13] }

// NG
filteredObject(
  {
    ten: 10,
    aaa: 'aaa',
    fibonacci: [1, 1, 2, 3, 5, 8, 13],
  },
  ['ten', 'bbb']  // Type 'bbb' is not assignable to type 'ten' | 'aaa' | 'fibonacci'
);
```

# 発展

また、これをさらに拡張して、ある特定の型から特定のキーを除外したオブジェクト型を作成したい場合を考えてみます。

どういうことかと言いますと、たとえば以下のような型があったとして、`extends`によって継承された `BaseInterface` のキーを除外し、`AccountConfig` や `ArticleConfig` 独自のプロパティだけを持つオブジェクト型を作成したい、というケースです。

```ts
interface BaseInterface {
  uuid: string;
  updatedAt: string;
}

const ExcludeBaseKeys = ['uuid', 'updatedAt'] as const;

interface AccountConfig extends BaseInterface {
  name: string;
  age: number;
  sex: 'male' | 'female';
}

const accountConfigData: AccountConfig = {
  uuid: crypto.randomUUID(),
  updatedAt: new Date().toISOString(),
  name: 'masayasviel',
  age: 999,
  sex: 'other'
}

filteredObject(
  accountConfigData,
  ExcludeBaseKeys,
);
// {
//   name: 'masayasviel',
//   age: 999,
//   sex: 'other'
// }

interface ArticleConfig extends BaseInterface {
  title: string;
  contents: string;
  tags: string[];
}

const articleConfigData: ArticleConfig = {
  uuid: crypto.randomUUID(),
  updatedAt: new Date().toISOString(),
  title: '指定キーを取り除いたオブジェクトを再作成する',
  contents: 'TSTSTSTSTSTSTSTSTSTS'
}

filteredObject(
  articleConfigData,
  ExcludeBaseKeys,
);
// {
//   title: '指定キーを取り除いたオブジェクトを再作成する',
//   contents: 'TSTSTSTSTSTSTSTSTSTS'
// }
```

このとき、 `ExcludeBaseKeys` は `BaseInterface` のキーを1つずつすべて持っていて欲しくなります。
これを解消するために、 [type-challenges/Union to Tuple](https://github.com/type-challenges/type-challenges/blob/main/questions/00730-hard-union-to-tuple/README.md)を用います。
これがどういうものかと言いますと、ユニオンで定義している型からタプル型を作成するものになります。
これで、`BaseInterface` のキーの定義し忘れがなくなります。

```ts
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type LastOf<T> = UnionToIntersection<T extends unknown ? () => T : never> extends () => infer L ? L : never;
type UnionToTuple<U, Last = LastOf<U>> = [U] extends [never]
  ? []
  : [...UnionToTuple<Exclude<U, Last>>, Last];

interface BaseInterface {
  uuid: string;
  updatedAt: string;
}
const ExcludeBaseKeys: UnionToTuple<keyof BaseInterface> = ['uuid', 'updatedAt'];
```

合併型を交差型に変換して、交差型の特徴を活かして最後の値を取得。
再帰させて順序担保のタプル型を作成しています。

# 最終結果

```ts
/** --- 実装 --- */
const strictEntries = <T extends Record<string, any>>(data: T): [keyof T, T[keyof T]][] => {
  return Object.entries(data);
};

const filteredObject = <T extends Record<string, any>, K extends keyof T>(originalObject: T, removeKeys: K[]) =>
  Object.fromEntries(strictEntries(originalObject).filter(([key, _]) => !removeKeys.includes(key as K))) as Omit<T, K>;

/** --- 発展 --- */
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type LastOf<T> = UnionToIntersection<T extends unknown ? () => T : never> extends () => infer L ? L : never;
type UnionToTuple<U, Last = LastOf<U>> = [U] extends [never]
  ? []
  : [...UnionToTuple<Exclude<U, Last>>, Last];

interface BaseInterface {
  uuid: string;
  updatedAt: string;
}
const ExcludeBaseKeys: UnionToTuple<keyof BaseInterface> = ['uuid', 'updatedAt'];

/** --- 実践 --- */

interface AccountConfig extends BaseInterface {
  name: string;
  age: number;
  sex: 'male' | 'female' | 'other';
}

const accountConfigData: AccountConfig = {
  uuid: crypto.randomUUID(),
  updatedAt: new Date().toISOString(),
  name: 'masayasviel',
  age: 999,
  sex: 'other'
}

filteredObject(
  accountConfigData,
  ExcludeBaseKeys,
);
// {
//   name: 'masayasviel',
//   age: 999,
//   sex: 'other'
// }

interface ArticleConfig extends BaseInterface {
  title: string;
  contents: string;
}

const articleConfigData: ArticleConfig = {
  uuid: crypto.randomUUID(),
  updatedAt: new Date().toISOString(),
  title: '指定キーを取り除いたオブジェクトを再作成する',
  contents: 'TSTSTSTSTSTSTSTSTSTS'
}

filteredObject(
  articleConfigData,
  ExcludeBaseKeys,
);
// {
//   title: '指定キーを取り除いたオブジェクトを再作成する',
//   contents: 'TSTSTSTSTSTSTSTSTSTS'
// }
```

# おわりに

生成AIの時代に、生成AIに適切なプロンプトを投げれば一発で出力されるようなことを記事にしています(といいつつ本記事は実装も文章も生成AIをめちゃくちゃ頼っています)