0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

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

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

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>;

使用方法

// 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 のキーを除外し、AccountConfigArticleConfig 独自のプロパティだけを持つオブジェクト型を作成したい、というケースです。

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'
// }

このとき、 ExcludeBaseKeysBaseInterface のキーを1つずつすべて持っていて欲しくなります。
これを解消するために、 type-challenges/Union to Tupleを用います。
これがどういうものかと言いますと、ユニオンで定義している型からタプル型を作成するものになります。
これで、BaseInterface のキーの定義し忘れがなくなります。

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'];

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

最終結果

/** --- 実装 --- */
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をめちゃくちゃ頼っています)

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?