Skip to content

fix(compat/values): treat sparse array holes as undefined for compatibility - #1894

Merged
raon0211 merged 1 commit into
toss:mainfrom
chuenchen309:fix/compat-values-sparse-holes
Jul 18, 2026
Merged

raon0211 merged 1 commit into
toss:mainfrom
chuenchen309:fix/compat-values-sparse-holes

Conversation

@chuenchen309

Copy link
Copy Markdown
Contributor

Summary

compat/values disagrees with lodash on sparse arrays. values returns Object.values(object), and the built-in skips holes; lodash's values is baseValues(object, keys(object)), which materializes every index.

This is the same class of fix as #1773 (compat/difference), applied to values.

Reproduce

import { values as loValues } from 'lodash';
import { values as esCompatValues } from './src/compat/object/values';

const array = [10, , 30];

console.log(loValues(array));       // [10, undefined, 30]
console.log(esCompatValues(array)); // [10, 30]

It also breaks an invariant inside compat, without needing lodash as a reference — keys and values disagree about the same object:

import { keys, values, toPairs } from './src/compat/index';

const array = [10, , 30];

keys(array);    // ['0', '1', '2']                       length 3
toPairs(array); // [['0',10], ['1',undefined], ['2',30]] length 3
values(array);  // [10, 30]                              length 2   ← the odd one out

So zipObject(keys(o), values(o)) pairs key '1' with 30 and key '2' with undefined.

Changes

  • values now reads through keys instead of Object.values. keys/arrayLikeKeys already builds times(object.length, ...) and returns every index for an array, so this makes values use the same iteration model rather than adding a second one.
  • Uses a for loop and adds no dependencies.
  • Added a regression test in values.spec.ts, mirroring the one from fix(compat/difference): treat sparse array holes as undefined for compability #1773. It asserts both the lodash-matching value and the keys().length === values().length invariant.

Behavior for dense arrays, plain objects, array-likes ({0:'a',1:'b',length:2} → ['a','b',2], the length leaking in exactly as lodash does), strings, and arguments objects is unchanged — the existing tests cover all of those and still pass.

Test results

$ yarn vitest run src/compat/object/values
 Test Files  1 passed (1)
      Tests  10 passed (10)

$ yarn vitest run
 Test Files  641 passed (641)
      Tests  4425 passed | 2 skipped (4427)

Baseline on main is 4424 passed | 2 skipped; the extra passing test is the new one. yarn tsc --noEmit is clean. I didn't run yarn lint --fix: yarn lint reports the same 2477 problems (8 errors, 2469 warnings) on untouched main as it does with this change, so none of them are from this PR.


Disclosure: I found this with the help of an AI coding assistant, by differential-testing compat against lodash 4.18.1. The repros and test runs above are ones I ran myself against this branch.

…bility

`values` returned `Object.values(object)`, and the built-in skips array holes.
Lodash's `values` is `baseValues(object, keys(object))`, so it materializes
every index:

    const array = [10, , 30];

    lodash.values(array)   // [10, undefined, 30]
    compat.values(array)   // [10, 30]

compat's own `keys` already models this correctly -- `arrayLikeKeys` builds
`times(object.length, ...)` and returns every index for an array -- so `values`
was the only side left on the built-in, and `keys(a).length === values(a).length`
did not hold for a sparse array. `zipObject(keys(o), values(o))` silently paired
the wrong values with the wrong keys.

Read through `keys` instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
es-toolkit Ready Ready Preview, Comment Jul 17, 2026 6:18am

Request Review

@raon0211 raon0211 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@raon0211
raon0211 merged commit 8ed0de3 into toss:main Jul 18, 2026
12 checks passed
gwennlbh pushed a commit to cigaleapp/cigale that referenced this pull request Aug 3, 2026
##### [v1.50.0](https://github.com/toss/es-toolkit/blob/HEAD/CHANGELOG.md#Version-v1500)

Released on July 24th, 2026.

- Added the `es-toolkit/types` entrypoint: a declaration-only module of compile-time
  type utilities TypeScript lacks natively — `ValueOf`, `Simplify`, `Writable`,
  `NonEmptyArray`, `DeepPartial`, and `DeepReadonly`. (\[[#1818](toss/es-toolkit#1818)])
- Added `flow` to `es-toolkit/fp`, a reusable data-last composition built on `pipe`. (\[[#1812](toss/es-toolkit#1812)])

The following bring `es-toolkit/compat` closer to Lodash. Behavior for the affected
edge cases now matches Lodash, so results may differ if you relied on the previous output.

- Fixed `compat/intersectionBy` to dedupe a single array (including by the iteratee)
  and to keep results consistent with Lodash. (\[[#1935](toss/es-toolkit#1935)])

- Fixed `compat/findIndex` and `compat/findLastIndex` to coerce a `NaN` `fromIndex`
  to `0` and to convert `fromIndex` to an integer. (\[[#1828](toss/es-toolkit#1828)], \[[#1834](toss/es-toolkit#1834)], \[[#1938](toss/es-toolkit#1938)])

- Fixed `compat/lastIndexOf` to coerce `fromIndex` like Lodash. (\[[#1832](toss/es-toolkit#1832)])

- Fixed `compat/every` to treat a falsy, non-nullish `doesMatch` as a `_.property`
  shorthand instead of identity. (\[[#1940](toss/es-toolkit#1940)])

- Fixed `compat/nth` to support string inputs. (\[[#1833](toss/es-toolkit#1833)])

- Fixed `compat/inRange` to not throw on a lone negative bound. (\[[#1835](toss/es-toolkit#1835)])

- Fixed `compat/toArray` to convert `Set`s to arrays like Lodash. (\[[#1840](toss/es-toolkit#1840)])

- Fixed `compat/values` to treat sparse array holes as `undefined`. (\[[#1894](toss/es-toolkit#1894)])

- Fixed `compat/maxBy` and `compat/minBy` to restore Lodash-compatible comparison. (\[[#1893](toss/es-toolkit#1893)])

- Fixed `compat/includes` to exclude the `length` property when matching array-like values. (\[[#1886](toss/es-toolkit#1886)])

- Fixed the case functions to split ordinal numbers like Lodash, and to count string
  size and padding by code points for multi-byte characters. (\[[#1836](toss/es-toolkit#1836)], \[[#1852](toss/es-toolkit#1852)], \[[#1853](toss/es-toolkit#1853)])

- Fixed `compat/unset` to not treat an own literal dotted key as a deep path. (\[[#1808](toss/es-toolkit#1808)])

- Moved `compat/flattenDepth`'s flattening logic out of `flatten`. (\[[#1847](toss/es-toolkit#1847)])

- Restored the standalone `eq` and `templateSettings` entry points. (\[[#1895](toss/es-toolkit#1895)])

- Fixed `deburr` to remove all combining diacritical marks, matching Lodash. (\[[#1807](toss/es-toolkit#1807)])

- Fixed `partition` to accept any predicate return value, like `Array.prototype.filter`. (\[[#1937](toss/es-toolkit#1937)])

- Fixed `has` and `hasIn` to prioritize nullish literal keys over paths. (\[[#1810](toss/es-toolkit#1810)])

- Fixed `invokeMap` to normalize a string path via `toPath` for `this` binding. (\[[#1814](toss/es-toolkit#1814)])

- Fixed `isDeepKey` to detect deep keys with stricter patterns. (\[[#1621](toss/es-toolkit#1621)])

- Fixed `unzipWith` to return an empty array instead of throwing on empty input. (\[[#1816](toss/es-toolkit#1816)])

- Improved documentation, JSDoc accuracy, and internal refactoring across `compat`
  and `fp`. (\[[#1817](toss/es-toolkit#1817)], \[[#1820](toss/es-toolkit#1820)], \[[#1822](toss/es-toolkit#1822)], \[[#1823](toss/es-toolkit#1823)], \[[#1825](toss/es-toolkit#1825)], \[[#1829](toss/es-toolkit#1829)], \[[#1830](toss/es-toolkit#1830)], \[[#1842](toss/es-toolkit#1842)],
  \[[#1845](toss/es-toolkit#1845)], \[[#1851](toss/es-toolkit#1851)], \[[#1858](toss/es-toolkit#1858)], \[[#1867](toss/es-toolkit#1867)], \[[#1879](toss/es-toolkit#1879)])

We sincerely thank [@Antoliny0919](https://github.com/Antoliny0919), [@raon0211](https://github.com/raon0211), [@dayongkr](https://github.com/dayongkr), [@spokodev](https://github.com/spokodev), [@sarathfrancis90](https://github.com/sarathfrancis90),
[@mayur-shenoy](https://github.com/mayur-shenoy), [@mahirhir](https://github.com/mahirhir), [@kojesung](https://github.com/kojesung), [@Hprogram](https://github.com/Hprogram), [@BangDori](https://github.com/BangDori), and everyone else who
contributed. We appreciate your great efforts!

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — ea7bed44 Deployed Jul 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants