Skip to content

Latest commit

 

History

History
325 lines (233 loc) · 16.1 KB

README.md

File metadata and controls

325 lines (233 loc) · 16.1 KB

ECMAScript Proposal: Object.propertyCount

Status

Champion: Jordan Harband

Author: Ruben Bridgewater [email protected]

Stage: 0

Overview

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.

Motivation

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.

Concrete usage examples

I only searched for Object.keys().length, since that is the most common one.

Angular

And multiple more.

React

Node.js

Minimatch

https://github.com/isaacs/minimatch/blob/0569cd3373408f9d701d3aab187b3f43a24a0db7/src/index.ts#L158

Vue

Lodash

Lodash uses an own implementation that behaves as Object.keys()

Other popular ones

Almost all popular JS/TS modules make use of this pattern.

Problem Statement

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.

Proposed API

Object.propertyCount(target[, options])

Parameters

  • target: The object whose properties will be counted.
    • Throws TypeError if target is not an object.
  • 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 with Object.keys).
      • Throws TypeError if provided invalid 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.

Detailed Examples and Edge Cases

  • 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

Explicit Semantics

  • Only own properties are considered.
  • Enumerability explicitly defined by the enumerable parameter.
  • Avoids intermediate array allocation entirely when implemented natively.

Algorithmic Specification

The native implementation should strictly avoid creating intermediate arrays or unnecessary allocations:

  1. Initialize a numeric property counter to 0.
  2. 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.
  3. Return the final count value.

See the spec proposal for details.

Alternatives Considered

  • Multiple separate methods: Rejected due to increased cognitive load and API complexity.

TC39 Stages and Champion

  • Ready for Stage 1 (proposal)

Use Cases

  • Improved readability and explicit intent
  • Significant performance gains
  • Reduced memory overhead
  • Simpler code

Precedent

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.

Polyfill

// 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;
};

Considerations

  • 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.

Conclusion

Object.propertyCount offers substantial performance benefits by efficiently counting object properties without intermediate arrays, enhancing ECMAScript with clarity, performance, and reduced memory overhead.