2
\$\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!

New contributor
medsmh is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
7
  • \$\begingroup\$ Why not add primary keys on database table and do an INSERT IGNORE? \$\endgroup\$
    – Thallius
    Nov 22 at 15:02
  • \$\begingroup\$ thanks for your comment, I updated my question :) This is a Vue app (frontend) \$\endgroup\$
    – medsmh
    Nov 22 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 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 at 0:04
  • \$\begingroup\$ @jdt I think the OP provided just an example, there might be many more props to consider \$\endgroup\$
    – Question3r
    Nov 23 at 7:04

Your Answer

medsmh is a new contributor. Be nice, and check out our Code of Conduct.

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.