3
\$\begingroup\$

I'm using TypeScript for my Vue app and have a custom type I'm working with

type MyCustomType = {
    fooProperty: string,
    barProperty: string
    /* ... other fields ... */
};

This type holds a bunch of properties acting as a "composite key". So the combination of all properties must be unique.


First approach:

The first idea coming to my mind is to simply use a Set<T>

const set: Set<MyCustomType> = new Set<MyCustomType>();

set.add({ fooProperty: "a", barProperty: "b" });

console.log(set.has({ fooProperty: "a", barProperty: "b" })); // expected: true, actual: false

The problem is that this set is not able to compare the objects. So { fooProperty: "a", barProperty: "b" } === { fooProperty: "a", barProperty: "b" } // false ( based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality )


Second approach:

An internal Map<string, T> holding T as a stringified key might be a good idea

class TrulyUniqueSet<ElementType> {
    private readonly elements: Map<string, ElementType> = new Map<string, ElementType>();

    private elementToKey(element: ElementType): string {
        return JSON.stringify(element);
    }

    public get size(): number {
        return this.elements.size;
    }
 
    public add(element: ElementType): TrulyUniqueSet<ElementType> {
        const elementKey: string = this.elementToKey(element);
 
        this.elements.set(elementKey, element);
        
        return this;
    }

    public clear(): void {
        this.elements.clear();
    }
 
    public delete(element: ElementType): boolean {
        const elementKey: string = this.elementToKey(element);
 
        return this.elements.delete(elementKey);
    }
    
    public has(element: ElementType): boolean {
        const elementKey: string = this.elementToKey(element);
 
        return this.elements.has(elementKey);
    }
 
    public values(): IterableIterator<ElementType> {
        return this.elements.values();
    }

    public forEach(callbackfn: (element: ElementType) => void): void {
        return this.elements.forEach((element: ElementType) => callbackfn(element));
    }
}

but things might get buggy if the structure of an object changes

const set: TrulyUniqueSet<MyCustomType> = new TrulyUniqueSet<MyCustomType>();

set.add({ fooProperty: "a", barProperty: "b" });

console.log(set.has({ fooProperty: "a", barProperty: "b" })); // expected: true, actual: true

console.log(set.has({ barProperty: "b", fooProperty: "a" })); // expected: true, actual: false

obviously because of different JSON strings.


Third approach:

I tried to extend the Set by overriding the equality check

class MyCustomTypeCollection extends Set<MyCustomType> {
    private customTypesAreEqual(firstValue: MyCustomType, secondValue: MyCustomType): boolean {
        return firstValue.fooProperty === secondValue.fooProperty &&
               firstValue.barProperty=== secondValue.barProperty;
               /* add other equality checks here */
    }
    
    public add(value: MyCustomType): this {
        if (!this.has(value)) {
            super.add(value);    
        }
        
        return this;
    }
 
    public has(value: MyCustomType): boolean { // truly check for duplicates
        return Array
            .from(this)
            .some((currentValue: MyCustomType) => this.customTypesAreEqual(currentValue, value));
    }
 
    public delete(value: MyCustomType): boolean {
        const filteredValues: MyCustomType[] = Array
            .from(this)
            .filter((currentValue: MyCustomType) => !this.customTypesAreEqual(currentValue, value)); // remove the value from the collection
 
        if(filteredValues.length < this.size) { // "rebuild" the collection
            this.clear();
 
            filteredValues.forEach(this.add);
 
            return true;
        }
 
        return false;
    }
}

The usage works as expected

const set: MyCustomTypeCollection = new MyCustomTypeCollection();

set.add({ fooProperty: "a", barProperty: "b" });

console.log(set.has({ fooProperty: "a", barProperty: "b" })); // expected: true, actual: true

console.log(set.has({ barProperty: "b", fooProperty: "a" })); // expected: true, actual: true

set.delete({ fooProperty: "a", barProperty: "b" });

console.log(set.has({ fooProperty: "a", barProperty: "b" })); // expected: false, actual: false

console.log(set.has({ barProperty: "b", fooProperty: "a" })); // expected: false, actual: false

but the implementation looks very slow in terms of performance.


I think I will go with the third approach but do you have any suggestions, better ideas or code improvements?

Thanks in advance!

\$\endgroup\$
7
  • \$\begingroup\$ Why not add primary keys on database table and do an INSERT IGNORE? \$\endgroup\$
    – Thallius
    Nov 22, 2021 at 15:02
  • \$\begingroup\$ thanks for your comment, I updated my question :) This is a Vue app (frontend) \$\endgroup\$
    – spa7Pdnt
    Nov 22, 2021 at 15:04
  • \$\begingroup\$ why not use a Map<T,Set<T>> with foo being the key for the map and bar in the set. \$\endgroup\$
    – jdt
    Nov 22, 2021 at 23:51
  • 1
    \$\begingroup\$ Eventually, there will be a record/tuple proposal, that would let you use composite data within sets. But you can only use immutable data (and it looks like you wanted mutability). Either way, you'd need a different solution for now, but if you're interested, here's the proposal. \$\endgroup\$ Nov 23, 2021 at 0:04
  • 1
    \$\begingroup\$ Set can hold and compare objects if you ask nicely. Objects are reference types in JS. So you shouldn't add an object literal but a reference to it. i.e. Do like var obj1 = {foo: "this", bar: "that"}; then mySet.add(obj1);. Now if you check for obj1 like mySet.has(obj1); you get true. If you have multiple sets you can simply put them into an array and refrence them individually like myObjArray[n] etc... \$\endgroup\$
    – Redu
    Nov 26, 2021 at 11:37

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.