Champion: Jordan Harband
Author: Ruben Bridgewater [email protected]
Stage: 0
This proposal introduces Object.propertyCount
, a built-in method to efficiently and intuitively obtain the count of an object's own properties, with support for distinguishing among indexed properties, string-keyed properties, symbol properties, enumerable and non-enumerable properties, without the performance overhead of intermediate array allocations of the object's keys.
Developers frequently rely on patterns like:
const obj = { a: 1, b: 2 };
const count = Object.keys(obj).length;
However, this approach creates unnecessary memory overhead and garbage collection pressure, as an intermediate array is allocated solely for counting properties. Highly-used runtimes, frameworks, and libraries (e.g., Node.js, React, Lodash, Angular, Storybook, Excalidraw, VS Code, Svelte, Next.js, three.js, Puppeteer, Tailwind, ...) frequently utilize Object.keys(obj).length
, compounding performance issues across applications.
For instance, React often counts props or state keys:
// React component example
const propCount = Object.keys(this.props).length;
Replacing these patterns with a native and optimized counting method significantly reduces memory overhead, garbage collection, and as such, runtime performance impacts.
I only searched for Object.keys().length
, since that is the most common one.
- https://github.com/angular/angular/blob/7499b74d7d2d6db132d1b19a73e13cf6e306e41e/packages/router/src/url_tree.ts#L119
- https://github.com/angular/angular/blob/7499b74d7d2d6db132d1b19a73e13cf6e306e41e/packages/core/src/transfer_state.ts#L120
- https://github.com/angular/angular/blob/7499b74d7d2d6db132d1b19a73e13cf6e306e41e/packages/compiler/src/render3/view/i18n/util.ts#L57
- https://github.com/angular/angular/blob/7499b74d7d2d6db132d1b19a73e13cf6e306e41e/packages/core/src/util/ng_dev_mode.ts#L97
And multiple more.
- https://github.com/facebook/react/blob/254dc4d9f37eb512d4ee8bad6a0fae7ae491caef/packages/shared/shallowEqual.js#L33C9-L35
- https://github.com/facebook/react/blob/254dc4d9f37eb512d4ee8bad6a0fae7ae491caef/packages/react-devtools-shared/src/hydration.js#L97
- https://github.com/facebook/react/blob/254dc4d9f37eb512d4ee8bad6a0fae7ae491caef/packages/react-reconciler/src/ReactFiberHydrationDiffs.js#L416-L417
- https://github.com/facebook/react/blob/254dc4d9f37eb512d4ee8bad6a0fae7ae491caef/packages/react-reconciler/src/ReactFiber.js#L677
- https://github.com/facebook/react/blob/254dc4d9f37eb512d4ee8bad6a0fae7ae491caef/packages/react-server/src/ReactFizzServer.js#L2384
- https://github.com/facebook/react/blob/254dc4d9f37eb512d4ee8bad6a0fae7ae491caef/packages/react-dom-bindings/src/client/ReactDOMComponent.js#L3138
- https://github.com/nodejs/node/blob/c3b6f949748b49ef25b0239bd4582d29976fdbad/lib/internal/util/comparisons.js#L385
- https://github.com/nodejs/node/blob/c3b6f949748b49ef25b0239bd4582d29976fdbad/lib/internal/util/comparisons.js#L754-L763
- https://github.com/nodejs/node/blob/c3b6f949748b49ef25b0239bd4582d29976fdbad/lib/internal/util/comparisons.js#L713-L720 (could be rewritten in a more performant way with the new API)
- https://github.com/nodejs/node/blob/c3b6f949748b49ef25b0239bd4582d29976fdbad/lib/internal/debugger/inspect_client.js#L248
- https://github.com/nodejs/node/blob/c3b6f949748b49ef25b0239bd4582d29976fdbad/lib/internal/cluster/primary.js#L146
- https://github.com/nodejs/node/blob/c3b6f949748b49ef25b0239bd4582d29976fdbad/lib/internal/console/constructor.js#L537
- https://github.com/vuejs/core/blob/d65b25cdda4c0e7fe8b51e000ecc3696baad0492/packages/shared/src/looseEqual.ts#L36C11-L37
- https://github.com/vuejs/core/blob/d65b25cdda4c0e7fe8b51e000ecc3696baad0492/rollup.config.js#L251
- https://github.com/vuejs/core/blob/d65b25cdda4c0e7fe8b51e000ecc3696baad0492/packages/compiler-core/src/utils.ts#L505
- https://github.com/vuejs/core/blob/d65b25cdda4c0e7fe8b51e000ecc3696baad0492/packages/runtime-core/src/customFormatter.ts#L123
- https://github.com/vuejs/core/blob/d65b25cdda4c0e7fe8b51e000ecc3696baad0492/packages/compiler-sfc/src/style/pluginScoped.ts#L33
- https://github.com/vuejs/core/blob/d65b25cdda4c0e7fe8b51e000ecc3696baad0492/packages/compiler-core/src/transforms/transformElement.ts#L905
Lodash uses an own implementation that behaves as Object.keys()
- https://github.com/lodash/lodash/blob/8a26eb42adb303f4adc7ef56e300f14c5992aa68/dist/lodash.js#L9921
- https://github.com/lodash/lodash/blob/8a26eb42adb303f4adc7ef56e300f14c5992aa68/dist/lodash.js#L11561
Almost all popular JS/TS modules make use of this pattern.
- https://github.com/trekhleb/javascript-algorithms/blob/e40a67b5d1aaf006622a90e2bda60043f4f66679/src/algorithms/graph/detect-cycle/detectDirectedCycle.js#L83
- https://github.com/storybookjs/storybook/blob/b91e25a25c8c1cc77ea6b316d03b4cce183d815c/code/core/src/theming/ensure.ts#L15
- https://github.com/storybookjs/storybook/blob/b91e25a25c8c1cc77ea6b316d03b4cce183d815c/code/core/src/theming/ensure.ts#L15
- https://github.com/storybookjs/storybook/blob/b91e25a25c8c1cc77ea6b316d03b4cce183d815c/code/lib/blocks/src/blocks/Controls.tsx#L60-L63
- https://github.com/storybookjs/storybook/blob/b91e25a25c8c1cc77ea6b316d03b4cce183d815c/scripts/sandbox/templates/root.ejs#L7
- https://github.com/storybookjs/storybook/blob/b91e25a25c8c1cc77ea6b316d03b4cce183d815c/code/lib/blocks/src/blocks/DocsPage.tsx#L15
- https://github.com/storybookjs/storybook/blob/b91e25a25c8c1cc77ea6b316d03b4cce183d815c/code/core/assets/server/template.ejs#L67
- https://github.com/tailwindlabs/tailwindcss/blob/e8715d081eac683d002892b8b3e13550f0276b45/packages/tailwindcss/src/compat/theme-variants.ts#L9
- https://github.com/tailwindlabs/tailwindcss/blob/e8715d081eac683d002892b8b3e13550f0276b45/packages/%40tailwindcss-upgrade/src/migrate-postcss.ts#L346
- https://github.com/tailwindlabs/tailwindcss/blob/e8715d081eac683d002892b8b3e13550f0276b45/packages/tailwindcss/src/compat/apply-compat-hooks.ts#L100
- https://github.com/puppeteer/puppeteer/blob/ff74c58464f985253b0a986f5fbbe4edc1658a42/packages/puppeteer-core/src/bidi/HTTPRequest.ts#L149
- https://github.com/puppeteer/puppeteer/blob/ff74c58464f985253b0a986f5fbbe4edc1658a42/packages/puppeteer-core/src/bidi/Page.ts#L623
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/actions/actionSelectAll.ts#L50
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/change.ts#L133
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/groups.ts#L38
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/components/App.tsx#L2760
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/actions/actionProperties.tsx#L1038
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/clipboard.ts#L317
- https://github.com/excalidraw/excalidraw/blob/e1bb59fb8f115cd8e75fcaaeefa03a81b0fdc697/packages/excalidraw/element/mutateElement.ts#L44
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/src/vs/base/common/equals.ts#L87-L91
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/src/vs/platform/policy/common/policy.ts#L37
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/build/lib/util.ts#L37
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/src/vs/platform/policy/node/nativePolicyService.ts#L26
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/extensions/terminal-suggest/src/fig/shared/utils.ts#L165
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/build/lib/i18n.ts#L90
- https://github.com/microsoft/vscode/blob/2e6728cc3b6ab7f2bc5223dd52abb5f3b595b827/src/vs/platform/product/common/product.ts#L61
- https://github.com/sveltejs/svelte/blob/f498a21063894e6e515e62d753396410624b2e0f/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js#L259
- https://github.com/mrdoob/three.js/blob/b0805c2a0fd46c137d605ba098bc2b17d507b46f/src/materials/ShaderMaterial.js#L359
- https://github.com/mrdoob/three.js/blob/b0805c2a0fd46c137d605ba098bc2b17d507b46f/editor/js/Sidebar.Geometry.BufferGeometry.js#L60
- https://github.com/mrdoob/three.js/blob/b0805c2a0fd46c137d605ba098bc2b17d507b46f/examples/jsm/loaders/3MFLoader.js#L239
- https://github.com/vercel/next.js/blob/5c5875105af06e17ccad4080a6ace137f14cdabb/packages/next/src/build/babel/loader/get-config.ts#L177
- https://github.com/vercel/next.js/blob/5c5875105af06e17ccad4080a6ace137f14cdabb/turbopack/packages/devlow-bench/src/cli.ts#L34
- https://github.com/vercel/next.js/blob/5c5875105af06e17ccad4080a6ace137f14cdabb/packages/next/check-error-codes.js#L31
- https://github.com/vercel/next.js/blob/5c5875105af06e17ccad4080a6ace137f14cdabb/scripts/trace-dd.mjs#L67
- https://github.com/vercel/next.js/blob/5c5875105af06e17ccad4080a6ace137f14cdabb/packages/next/src/server/lib/utils.ts#L243
- https://github.com/vercel/next.js/blob/5c5875105af06e17ccad4080a6ace137f14cdabb/scripts/trace-to-tree.mjs#L147
Currently, accurately counting object properties involves verbose and inefficient workarounds:
const count = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj)
].length;
const reflectCount = Reflect.ownKeys(obj).length;
assert.strictEqual(count, reflectCount);
This creates intermediate arrays, causing unnecessary memory usage and garbage collection, impacting application performance — especially at scale and in performance-critical code paths.
On top of that, it is also not possible to identify an array that is sparse without calling Object.keys()
(or similar). This API would allow that by explicitly checking for own index properties.
Object.propertyCount(target[, options])
target
: The object whose properties will be counted.- Throws
TypeError
if target is not an object.
- Throws
options
(optional): An object specifying filtering criteria:keyTypes
: Array specifying property types to include:- Possible values:
'index'
,'nonIndexString'
,'symbol'
. - Defaults to
['index', 'nonIndexString']
(aligning closely withObject.keys
). - Throws
TypeError
if provided invalid values.
- Possible values:
enumerable
: Indicates property enumerability:true
to count only enumerable properties (default).false
to count only non-enumerable properties.'all'
to count both enumerable and non-enumerable properties.- Throws
TypeError
if provided invalid values.
Defaults align closely with Object.keys
for ease of adoption, ensuring intuitive behavior without needing explicit configuration in common cases.
The naming of keyTypes and if it's an array or an object or the like is open for discussion. Important is just, that it's possible to differentiate index from non index strings somehow, as well as symbol properties.
Similar applies to the enumerable option: true, false, and 'all'
seems cleanest, but it's not important how they are named.
- Empty object:
Object.propertyCount({}); // returns 0
- Object without prototype:
const obj = Object.create(null);
obj.property = 1;
Object.propertyCount(obj); // returns 1
const obj2 = { __proto__: null });
obj2.property = 1;
Object.propertyCount(obj2); // returns 1
- Array index keys:
See https://tc39.es/ecma262/#array-index
let obj = { '01': 'string key', 1: index, 2: 'index' };
Object.propertyCount(obj, { keyTypes: ['index'] }); // returns 2
obj = { '0': 'index', '-1': 'string key', '01': 'string key' };
Object.propertyCount(obj, { keyTypes: ['index'] }); // returns 1 (only '0')
- String based keys:
const obj = { '01': 'string key', 1: 'index', 2: 'index' };
Object.propertyCount(obj, { keyTypes: ['nonIndexString'] }); // returns 1
- Symbol based keys:
const obj = { [Symbol()]: 'symbol', 1: 'index', 2: 'index' };
Object.propertyCount(obj, { keyTypes: ['symbol'] }); // returns 1
- Only own properties are considered.
- Enumerability explicitly defined by the
enumerable
parameter. - Avoids intermediate array allocation entirely when implemented natively.
The native implementation should strictly avoid creating intermediate arrays or unnecessary allocations:
- Initialize a numeric property counter to
0
. - Iterate directly over the object's own property descriptors
- Access the internal property keys directly via the object's internal slots.
- For each own property:
- Determine if the key is a numeric index, a regular non-index string, or a symbol.
- Check if the property type matches any specified in
keyTypes
. - If
enumerable
is not'all'
, match the property's enumerability against the provided boolean value. - If the property meets all criteria, increment the counter.
- Return the final count value.
See the spec proposal for details.
- Multiple separate methods: Rejected due to increased cognitive load and API complexity.
- Ready for Stage 1 (proposal)
- Improved readability and explicit intent
- Significant performance gains
- Reduced memory overhead
- Simpler code
Frequent patterns in widely-used JavaScript runtimes, frameworks, and libraries (Node.js, React, Angular, Lodash) demonstrate the common need for an optimized property counting mechanism.
// NOTE: do not use this polyfill in a production environment
const validTypes = new Set(['index', 'nonIndexString', 'symbol']);
Object.propertyCount = function (target, options) {
if (typeof target !== 'object' || target === null) {
throw new TypeError(`Expected target to be an object. Received ${typeof target}`);
}
if (options === undefined) {
return Object.keys(target).length;
}
const { keyTypes = ['index', 'nonIndexString'], enumerable = true } = options || {};
for (const type of keyTypes) {
if (!validTypes.has(type)) {
throw new TypeError(`Invalid property type (${type}) in 'keyTypes' option.`);
}
}
if (typeof enumerable !== 'boolean' && enumerable !== 'all') {
throw new TypeError(`Invalid input (${enumerable}) in 'enumerable' option.`);
}
let props = [];
if (keyTypes.includes('index') || keyTypes.includes('nonIndexString')) {
let stringProps = enumerable === true ? Object.keys(target) : Object.getOwnPropertyNames(target);
if (!keyTypes.includes('nonIndexString')) {
stringProps = stringProps.filter(key => String(parseInt(key, 10)) === key && parseInt(key, 10) >= 0);
} else if (!keyTypes.includes('index')) {
stringProps = stringProps.filter(key => String(parseInt(key, 10)) !== key || parseInt(key, 10) < 0);
}
props = stringProps;
}
if (keyTypes.includes('symbol')) {
props = props.concat(Object.getOwnPropertySymbols(target));
}
if (enumerable !== 'all') {
props = props.filter(key => Object.getOwnPropertyDescriptor(target, key).enumerable === enumerable);
}
return props.length;
};
- Backwards compatibility: Fully backward compatible.
- Performance: Native implementation will significantly outperform existing approaches by eliminating intermediate arrays.
- Flexibility: Enumerable properties counted by default; easy inclusion/exclusion.
- Simplicity: Improved code readability and clarity.
Object.propertyCount
offers substantial performance benefits by efficiently counting object properties without intermediate arrays, enhancing ECMAScript with clarity, performance, and reduced memory overhead.