본문으로 건너뛰기

uniqueWith

Removes duplicate elements from an array using a custom comparison function to determine equality. Creates a new array containing elements where no two elements are considered equal according to the provided comparison function. Only the first occurrence of elements considered equal is retained. This provides maximum flexibility for defining custom equality logic that cannot be expressed through simple value transformation.

Signature​

const uniqueWith: <Type>(source: Type[], isEqual: (item1: Type, item2: Type) => boolean) => Type[]

Parameters​

NameTypeDescription
source-Source array to remove duplicates from
isEqual-Comparison function to determine if two elements are equal

Returns​

Array with duplicates removed based on custom equality

Examples​

Case-insensitive string deduplication​

import { uniqueWith } from '@winglet/common-utils';

const names = ['Alice', 'BOB', 'alice', 'Charlie', 'bob', 'CHARLIE'];
const uniqueNames = uniqueWith(names, (a, b) => a.toLowerCase() === b.toLowerCase());
console.log(uniqueNames); // ['Alice', 'BOB', 'Charlie']

Object deduplication with multiple criteria​

interface Person {
firstName: string;
lastName: string;
age: number;
email: string;
}

const people: Person[] = [
{ firstName: 'John', lastName: 'Doe', age: 30, email: 'john@example.com' },
{ firstName: 'Jane', lastName: 'Smith', age: 25, email: 'jane@example.com' },
{ firstName: 'John', lastName: 'Doe', age: 31, email: 'john.doe@example.com' }, // Same name, different age/email
{ firstName: 'Bob', lastName: 'Johnson', age: 35, email: 'bob@example.com' }
];

// Remove duplicates based on first and last name only
const uniquePeople = uniqueWith(people, (a, b) =>
a.firstName === b.firstName && a.lastName === b.lastName
);
console.log(uniquePeople);
// [
// { firstName: 'John', lastName: 'Doe', age: 30, email: 'john@example.com' },
// { firstName: 'Jane', lastName: 'Smith', age: 25, email: 'jane@example.com' },
// { firstName: 'Bob', lastName: 'Johnson', age: 35, email: 'bob@example.com' }
// ]

Approximate numeric comparison​

const measurements = [1.0, 1.05, 2.0, 1.98, 3.0, 2.95, 4.0];
const tolerance = 0.1;

const uniqueMeasurements = uniqueWith(measurements, (a, b) =>
Math.abs(a - b) < tolerance
);
console.log(uniqueMeasurements); // [1.0, 2.0, 3.0, 4.0]
// Values within 0.1 of each other are considered duplicates

Date comparison ignoring time​

const events = [
{ name: 'Meeting', date: new Date('2024-01-15T09:00:00Z') },
{ name: 'Lunch', date: new Date('2024-01-15T12:00:00Z') },
{ name: 'Conference', date: new Date('2024-01-16T10:00:00Z') },
{ name: 'Workshop', date: new Date('2024-01-15T15:00:00Z') }
];

// Remove events on the same day (ignoring time)
const uniqueByDate = uniqueWith(events, (a, b) =>
a.date.toDateString() === b.date.toDateString()
);
console.log(uniqueByDate);
// [
// { name: 'Meeting', date: ... }, // Jan 15
// { name: 'Conference', date: ... } // Jan 16
// ]

Complex array comparison​

const coordinates = [
[1, 2, 3],
[4, 5, 6],
[1, 2, 3], // Duplicate
[7, 8, 9],
[4, 5, 6] // Duplicate
];

const uniqueCoordinates = uniqueWith(coordinates, (a, b) =>
a.length === b.length && a.every((val, idx) => val === b[idx])
);
console.log(uniqueCoordinates); // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Object deep comparison​

interface Product {
id: number;
specs: {
cpu: string;
ram: number;
};
}

const products: Product[] = [
{ id: 1, specs: { cpu: 'Intel i5', ram: 8 } },
{ id: 2, specs: { cpu: 'Intel i7', ram: 16 } },
{ id: 3, specs: { cpu: 'Intel i5', ram: 8 } }, // Same specs as id: 1
{ id: 4, specs: { cpu: 'AMD Ryzen', ram: 16 } }
];

// Remove products with identical specifications
const uniqueBySpecs = uniqueWith(products, (a, b) =>
a.specs.cpu === b.specs.cpu && a.specs.ram === b.specs.ram
);
console.log(uniqueBySpecs);
// [
// { id: 1, specs: { cpu: 'Intel i5', ram: 8 } },
// { id: 2, specs: { cpu: 'Intel i7', ram: 16 } },
// { id: 4, specs: { cpu: 'AMD Ryzen', ram: 16 } }
// ]

Geographic proximity deduplication​

interface Location {
name: string;
lat: number;
lng: number;
}

const locations: Location[] = [
{ name: 'Store A', lat: 40.7128, lng: -74.0060 },
{ name: 'Store B', lat: 40.7130, lng: -74.0062 }, // Very close to Store A
{ name: 'Store C', lat: 41.8781, lng: -87.6298 },
{ name: 'Store D', lat: 40.7129, lng: -74.0061 } // Very close to Store A
];

const proximityThreshold = 0.01; // Degrees

const uniqueLocations = uniqueWith(locations, (a, b) => {
const latDiff = Math.abs(a.lat - b.lat);
const lngDiff = Math.abs(a.lng - b.lng);
return latDiff < proximityThreshold && lngDiff < proximityThreshold;
});
console.log(uniqueLocations);
// [{ name: 'Store A', ... }, { name: 'Store C', ... }]

Version comparison with semantic versioning​

const versions = ['1.0.0', '1.0.1', '1.0.0', '2.0.0', '1.0.1', '1.1.0'];

const parseVersion = (version: string) => version.split('.').map(Number);
const compareVersions = (a: string, b: string) => {
const [aMajor, aMinor, aPatch] = parseVersion(a);
const [bMajor, bMinor, bPatch] = parseVersion(b);
return aMajor === bMajor && aMinor === bMinor && aPatch === bPatch;
};

const uniqueVersions = uniqueWith(versions, compareVersions);
console.log(uniqueVersions); // ['1.0.0', '1.0.1', '2.0.0', '1.1.0']

Custom object equality with partial matching​

interface Task {
id: number;
title: string;
priority: 'low' | 'medium' | 'high';
assignee: string;
}

const tasks: Task[] = [
{ id: 1, title: 'Fix bug', priority: 'high', assignee: 'Alice' },
{ id: 2, title: 'Add feature', priority: 'medium', assignee: 'Bob' },
{ id: 3, title: 'Fix bug', priority: 'high', assignee: 'Charlie' }, // Same title & priority
{ id: 4, title: 'Review code', priority: 'low', assignee: 'Alice' }
];

// Remove tasks with same title and priority (ignore assignee)
const uniqueTasks = uniqueWith(tasks, (a, b) =>
a.title === b.title && a.priority === b.priority
);
console.log(uniqueTasks);
// [
// { id: 1, title: 'Fix bug', priority: 'high', assignee: 'Alice' },
// { id: 2, title: 'Add feature', priority: 'medium', assignee: 'Bob' },
// { id: 4, title: 'Review code', priority: 'low', assignee: 'Alice' }
// ]

Playground​

import { uniqueWith } from '@winglet/common-utils';

const names = ['Alice', 'BOB', 'alice', 'Charlie', 'bob', 'CHARLIE'];
const uniqueNames = uniqueWith(names, (a, b) => a.toLowerCase() === b.toLowerCase());
console.log(uniqueNames); // ['Alice', 'BOB', 'Charlie']

Notes​

Performance: Uses nested loops with O(n²) time complexity in worst case where n is the array length. Consider using uniqueBy with a hash function for large arrays when possible.

Comparison Function: The isEqual function should be pure and symmetric. If isEqual(a, b) returns true, then isEqual(b, a) should also return true.

First Occurrence: When multiple elements are considered equal according to the comparison function, only the first occurrence is retained in the result.

Order Preservation: Maintains the original order of elements from the source array in the result array.

Memory Efficiency: Uses a simple result array with direct indexing. The comparison function is called for each pair until a match is found or all comparisons are complete.