Uenishi.Web

大阪に生息しているプログラマーのブログ

型安全にオブジェクト同士を比較する

この記事は?

業務でオブジェクト同士を比較して、valueに差分があればdifference、差分がなければsameとして検出する必要があったため、実装のメモ書きです。

TypeScriptで記述しているため、オブジェクトの型安全性も考慮した作りになっています。

使用例

比較したいオブジェクトの例。 いい感じに、Profile型のオブジェクトを定義します。

type Profile = {
  name: string,
  address: string,
  email: string,
  age: number,
}

const obj1: Profile = {
  name: "太郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 25,
}

const obj2: Profile = {
  name: "次郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 23,

実装は以下です。

obj1に対して、obj2に違うvalueがあればそのプロパティをdifferentとして返却するようになっています。


type Result<T> = {
  different: Partial<T>;
  same: Partial<T>;
};

export function compareObjectDiff<T extends object>(obj1: T, obj2: T): Result<T> {
  const different: Partial<T> = {};
  const same: Partial<T> = {};

  const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);

  keys.forEach((key) => {
    const keyTyped = key as keyof T;

    if (obj1[keyTyped] === obj2[keyTyped]) {
      same[keyTyped] = obj1[keyTyped];
    } else {
      different[keyTyped] = obj2[keyTyped];
    }
  });
  return { different, same };
}

const diff = compareObjectDiff(obj1, obj2)

console.log(diff)

結果

{
  "different": {
    "name": "次郎",
    "age": 23
  },
  "same": {
    "address": "東京",
    "email": "taro@gmail.com"
  }
}

ひとつひとつ、何の処理をしているのか読み取っていきます。

// 戻り値の型定義 different, sameはT型のサブセット(オブジェクトの一部分)であるため
// Partial<T>で定義する。
type Result<T> = {
  different: Partial<T>;
  same: Partial<T>;
};

// 引数としてobjectを継承したジェネリクス(T)型を受け取る。
// extends object とすることで、オブジェクト型以外が渡されることがないよう制約を加える。
export function compareObjectDiff<T extends object>(obj1: T, obj2: T): Result<T> {

	// 戻り値のオブジェクトを定義
  const different: Partial<T> = {};
  const same: Partial<T> = {};

  // Setオブジェクトにkeyを格納(Set内の値はコレクション内で一意になる)
  // obj1, obj2のkeyをそれぞれスプレッド構文を用いて展開する。
  const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);

  keys.forEach((key) => {
		// keyof T とすることで、T型のプロパティとして型チェックを行う
		// この例だと、keyTypedは "name" | "address" | "email" | "age" 型となる
    const keyTyped = key as keyof T;

		// オブジェクトを比較。keyTypedで、obj1, obj2の同じプロパティのvalueを検証している。
		// 一致していればsame, 一致していなければdifferentに相違している値を代入する
    if (obj1[keyTyped] === obj2[keyTyped]) {
      same[keyTyped] = obj1[keyTyped];
    } else {
      different[keyTyped] = obj2[keyTyped];
    }
  });
  return { different, same };
}

const diff = compareObjectDiff(obj1, obj2)

console.log(diff)

ただ、この実装では例えば以下のように入れ子になったオブジェクトは検証することができません。

const obj3: Favorite = {
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

const obj4: Profile & Favorite = {
  name: "太郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 25,
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

const obj5: Profile & Favorite = {
  name: "次郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 23,
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

type Result<T> = {
  different: Partial<T>;
  same: Partial<T>;
};

function compareObjectDiff<T extends object>(before: T, after: T): Result<T> {
  const different: Partial<T> = {};
  const same: Partial<T> = {};

  const keys = new Set([...Object.keys(before), ...Object.keys(after)]);

  keys.forEach((key) => {
    const keyTyped = key as keyof T;

    if (before[keyTyped] === after[keyTyped]) {
      same[keyTyped] = before[keyTyped];
    } else {
      different[keyTyped] = after[keyTyped];
    }
  });
  return { different, same };
}

const diff = compareObjectDiff(obj4, obj5)

console.log(diff)
const obj3: Favorite = {
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

const obj4: Profile & Favorite = {
  name: "太郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 25,
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

const obj5: Profile & Favorite = {
  name: "次郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 23,
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

type Result<T> = {
  different: Partial<T>;
  same: Partial<T>;
};

function compareObjectDiff<T extends object>(before: T, after: T): Result<T> {
  const different: Partial<T> = {};
  const same: Partial<T> = {};

  const keys = new Set([...Object.keys(before), ...Object.keys(after)]);

  keys.forEach((key) => {
    const keyTyped = key as keyof T;

    if (before[keyTyped] === after[keyTyped]) {
      same[keyTyped] = before[keyTyped];
    } else {
      different[keyTyped] = after[keyTyped];
    }
  });
  return { different, same };
}

const diff = compareObjectDiff(obj4, obj5)

console.log(diff)

結果(中身は同じなのに、defferentにはいってしまう)

{
  "different": {
    "name": "次郎",
    "age": 23,
    "favorite": {
      "food": "ramen",
      "hobby": "travel",
      "other": "たまに筋トレ"
    }
  },
  "same": {
    "address": "東京",
    "email": "taro@gmail.com"
  }
}

入れ子になったオブジェクトも判定するには、以下のように再帰的に関数を呼び出すようにすると判定できるようになります。



type Profile = {
  name: string,
  address: string,
  email: string,
  age: number,
}

type Favorite = {
  favorite: {
    food: string,
    hobby: string,
    other: string
  }
}

const obj4: Profile & Favorite = {
  name: "太郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 25,
  favorite: {
    food: "ramen",
    hobby: "food",
    other: "たまに筋トレ"
  }
}

const obj5: Profile & Favorite = {
  name: "次郎",
  address: "東京",
  email: "taro@gmail.com",
  age: 23,
  favorite: {
    food: "ramen",
    hobby: "travel",
    other: "たまに筋トレ"
  }
}

type Result<T> = {
  different: {[K in keyof T]?: T[K] extends object ? Result<T[K]> : { value: T[K]; }};
  same: {[K in keyof T]?: T[K] };
};

function compareObjectDiff<T extends object>(before: T, after: T): Result<T> {
  const different: { [K in keyof T]?: T[K] extends object ? Result<T[K]> : { value: T[K]; } } = {};
  const same: { [K in keyof T]?: T[K]} = {};

  const keys = new Set([...Object.keys(before), ...Object.keys(after)]);

  keys.forEach((key) => {
    const keyTyped = key as keyof T;

    if (before[keyTyped] === after[keyTyped]) {
      same[keyTyped] = before[keyTyped];
    } else if (
      before[keyTyped] &&
      after[keyTyped] &&
      typeof before[keyTyped] === 'object' &&
      typeof after[keyTyped] === 'object'
    ) {
      different[keyTyped] = compareObjectDiff(before[keyTyped], after[keyTyped]) as any
    } else {
      different[keyTyped] = after[keyTyped];
    }
  });
  return { different, same };
}

const diff = compareObjectDiff(obj4, obj5)

console.log(diff)

出力

{
  "different": {
    "name": "次郎",
    "age": 23,
    "favorite": {
      "different": {
        "hobby": "travel"
      },
      "same": {
        "food": "ramen",
        "other": "たまに筋トレ"
      }
    }
  },
  "same": {
    "address": "東京",
    "email": "taro@gmail.com"
  }
}

「型安全に」と言いつつも、敗北のanyを使用していまっている……。

よりよい書き方があれば今後改善しようと思います。