unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
vuejs/core
mid7/10
data-structurestesting-strategyerror-handling

fix(shared): correctly compare Map and Set values

Project: vuejs/core | Language: TypeScript | Concepts: data-structures, testing-strategy, error-handling | Difficulty: mid
Link: https://github.com/vuejs/core/pull/15328

The problem

The looseEqual utility did not correctly compare Map and Set values. It would fail to consider two maps/sets with the same contents but different key/value object identities or orderings as equal. This caused issues such as incorrect selection in v-model bindings when the value was a Map.

Why it's non-trivial

A naive comparison would only check reference equality or use shallow comparison, which does not account for:

  • Different object identities for keys/values that are otherwise "loosely equal"
  • Different orderings of entries in Map or Set
  • The need to recursively compare collection contents, not just their structure

The approach

1. Add collection type guards

The imports now include isMap and isSet:

-import { isArray, isDate, isObject, isSymbol } from './general'
+import { isArray, isDate, isMap, isObject, isSet, isSymbol } from './general'

This enables the function to detect when it is dealing with a Map or Set.


2. Implement a generic collection comparator

A new helper function compares Map and Set contents by size and recursively matches each entry:

function looseCompareCollections(
  a: Map<any, any> | Set<any>,
  b: Map<any, any> | Set<any>,
) {
  if (a.size !== b.size) return false
  const candidates = Array.from(b)
  const matched = new Uint8Array(candidates.length)
  for (const item of a) {
    let index = -1
    for (let i = 0; i < candidates.length; i++) {
      if (!matched[i] && looseEqual(item, candidates[i])) {
        index = i
        break
      }
    }
    if (index < 0) return false
    matched[index] = 1
  }
  return true
}

This function ensures that every item in one collection has a loosely equal match in the other, regardless of order or object identity.


3. Integrate collection comparison into looseEqual

The main function now checks for Map and Set types and uses the new comparator:

+    aValidType = isMap(a)
+    bValidType = isMap(b)
+    if (aValidType || bValidType) {
+      return aValidType && bValidType ? looseCompareCollections(a, b) : false
+    }
+    aValidType = isSet(a)
+    bValidType = isSet(b)
+    if (aValidType || bValidType) {
+      return aValidType && bValidType ? looseCompareCollections(a, b) : false
+    }

This ensures that two collections are only considered equal if they are of the same type and have equal contents.


4. Add thorough tests

Tests now cover various scenarios for Map and Set equality, including:

  • Same/different contents
  • Different orderings
  • Loosely equal keys/values

Example:

test('compares maps correctly', () => {
  const map = new Map<any, any>([
    [{ id: 1 }, { value: '1' }],
    ['foo', 'bar'],
  ])

  expect(looseEqual(map, map)).toBe(true)
  expect(
    looseEqual(
      map,
      new Map<any, any>([
        ['foo', 'bar'],
        [{ id: '1' }, { value: 1 }],
      ]),
    ),
  ).toBe(true)
  // ... more cases
})

This ensures the logic is correct and robust for real-world usage.


5. Regression test for v-model with Map values

A test ensures that the correct <option> is selected when the bound value is a Map:

it('selects the matching option when values are maps', () => {
  const mapA = new Map([['id', 1]])
  const mapB = new Map([['id', 2]])
  const value = ref(mapB)
  // ... render select with options using mapA and mapB
  expect(root.querySelector('select').selectedIndex).toBe(1)
})

This prevents regressions in how forms handle complex value types.

From the review thread

No notable review discussion on this PR.

Takeaways

  • When comparing complex data structures like Map and Set, always compare contents recursively, not just references or order.
  • Good tests should cover edge cases, including different orderings and "loose" equality of keys/values.
  • Fixes for utility functions should include regression tests for real-world usage (such as form bindings) to prevent subtle bugs.