Skip to content

Commit 6e07090

Browse files
feat: _firstKey, _firstValue and related optimized functions
1 parent 91231ab commit 6e07090

5 files changed

Lines changed: 122 additions & 3 deletions

File tree

packages/js-lib/src/array/array.util.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
_find,
1818
_findLast,
1919
_first,
20+
_firstFromIterable,
2021
_firstLast,
2122
_firstLastOrUndefined,
2223
_firstOrUndefined,
@@ -460,6 +461,33 @@ test('_first', () => {
460461
expect(_first([1])).toBe(1)
461462
})
462463

464+
test('_firstFromIterable', () => {
465+
expect(_firstFromIterable([])).toBeUndefined()
466+
expect(_firstFromIterable([1, 2, 3])).toBe(1)
467+
expect(_firstFromIterable(new Set([1, 2, 3]))).toBe(1)
468+
expect(_firstFromIterable(new Set<number>())).toBeUndefined()
469+
expect(_firstFromIterable(new Map([['a', 1]]).values())).toBe(1)
470+
expect(
471+
_firstFromIterable(
472+
new Map([
473+
['a', 1],
474+
['b', 2],
475+
]).keys(),
476+
),
477+
).toBe('a')
478+
479+
function* gen(): Generator<string> {
480+
yield 'x'
481+
yield 'y'
482+
}
483+
484+
expect(_firstFromIterable(gen())).toBe('x')
485+
486+
function* emptyGen(): Generator<string> {}
487+
488+
expect(_firstFromIterable(emptyGen())).toBeUndefined()
489+
})
490+
463491
test('_min', () => {
464492
expect(_minOrUndefined([])).toBeUndefined()
465493
expect(_minOrUndefined([3])).toBe(3)

packages/js-lib/src/array/array.util.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,19 @@ export function _firstOrUndefined<T>(array: readonly T[]): T | undefined {
468468
return array[0]
469469
}
470470

471+
/**
472+
* Returns the first item of an iterable (Set, Map.values(), generator, etc.),
473+
* or `undefined` if the iterable is empty.
474+
*
475+
* Avoids the `Array.from(iter)[0]` pattern that materialises the entire iterable.
476+
* `for...of` with an early return advances the iterator exactly once; if the
477+
* iterator implements `return()` (generators, etc.), it is invoked for cleanup.
478+
*/
479+
export function _firstFromIterable<T>(iter: Iterable<T>): T | undefined {
480+
for (const item of iter) return item
481+
return undefined
482+
}
483+
471484
export function _minOrUndefined<T>(array: readonly T[]): NonNullable<T> | undefined {
472485
let min: NonNullable<T> | undefined
473486
for (const item of array) {

packages/js-lib/src/is.util.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,19 @@ export function _isPrimitive(v: any): v is Primitive {
4545
}
4646

4747
export function _isEmptyObject(obj: AnyObject): boolean {
48-
return Object.keys(obj).length === 0
48+
// for...in with early return avoids allocating the full Object.keys array.
49+
// Object.hasOwn matches Object.keys() semantics (own enumerable keys only).
50+
for (const k in obj) {
51+
if (Object.hasOwn(obj, k)) return false
52+
}
53+
return true
4954
}
5055

5156
export function _isNotEmptyObject(obj: AnyObject): boolean {
52-
return Object.keys(obj).length > 0
57+
for (const k in obj) {
58+
if (Object.hasOwn(obj, k)) return true
59+
}
60+
return false
5361
}
5462

5563
/**
@@ -74,7 +82,10 @@ export function _isEmpty(obj: any): boolean {
7482
}
7583

7684
if (typeof obj === 'object') {
77-
return Object.keys(obj).length === 0
85+
for (const k in obj) {
86+
if (Object.hasOwn(obj, k)) return false
87+
}
88+
return true
7889
}
7990

8091
return false

packages/js-lib/src/object/object.util.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
_filterObject,
1212
_filterUndefinedValues,
1313
_findKeyByValue,
14+
_firstEntry,
15+
_firstKey,
16+
_firstValue,
1417
_get,
1518
_has,
1619
_hasProp,
@@ -683,6 +686,29 @@ test('_findKeyByValue', () => {
683686
expect(char).toBe(CHAR.B)
684687
})
685688

689+
test('_firstKey', () => {
690+
expect(_firstKey({ a: 1, b: 2 })).toBe('a')
691+
expect(_firstKey({ x: 1 })).toBe('x')
692+
expect(_firstKey({})).toBeUndefined()
693+
// V8 sorts integer-like keys ascending first, then string keys in insertion order
694+
expect(_firstKey({ 2: 'b', 1: 'a' })).toBe('1')
695+
expect(_firstKey({ b: 1, 1: 'a' })).toBe('1')
696+
})
697+
698+
test('_firstValue', () => {
699+
expect(_firstValue({ a: 1, b: 2 })).toBe(1)
700+
expect(_firstValue({ x: 'only' })).toBe('only')
701+
expect(_firstValue({})).toBeUndefined()
702+
expect(_firstValue({ 2: 'b', 1: 'a' })).toBe('a')
703+
})
704+
705+
test('_firstEntry', () => {
706+
expect(_firstEntry({ a: 1, b: 2 })).toEqual(['a', 1])
707+
expect(_firstEntry({ x: 'only' })).toEqual(['x', 'only'])
708+
expect(_firstEntry({})).toBeUndefined()
709+
expect(_firstEntry({ 2: 'b', 1: 'a' })).toEqual(['1', 'a'])
710+
})
711+
686712
test('_deepFreeze', () => {
687713
const o = {
688714
a: {

packages/js-lib/src/object/object.util.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,47 @@ export function _findKeyByValue<T extends AnyObject>(obj: T, v: ValueOf<T>): key
242242
return Object.entries(obj).find(([_, value]) => value === v)?.[0] as keyof T
243243
}
244244

245+
/**
246+
* Returns the first key of the object, or `undefined` if the object is empty.
247+
*
248+
* Performance-optimised: uses `for...in` with an early return to avoid
249+
* allocating the full `Object.keys(obj)` array. The `Object.hasOwn` filter
250+
* matches `Object.keys()` semantics (own enumerable string keys only) and
251+
* satisfies the `guard-for-in` lint rule; cost is a single check before
252+
* the early return. Iteration order matches `Object.keys()` for plain
253+
* objects (integer-like keys ascending first, then string keys in
254+
* insertion order).
255+
*/
256+
export function _firstKey<T extends AnyObject>(obj: T): keyof T | undefined {
257+
for (const k in obj) {
258+
if (Object.hasOwn(obj, k)) return k as keyof T
259+
}
260+
return undefined
261+
}
262+
263+
/**
264+
* Returns the first value of the object, or `undefined` if the object is empty.
265+
* See `_firstKey` for the iteration-order contract.
266+
*/
267+
export function _firstValue<T extends AnyObject>(obj: T): ValueOf<T> | undefined {
268+
for (const k in obj) {
269+
if (Object.hasOwn(obj, k)) return obj[k] as ValueOf<T>
270+
}
271+
return undefined
272+
}
273+
274+
/**
275+
* Returns the first [key, value] tuple of the object,
276+
* or `undefined` if the object is empty.
277+
* See `_firstKey` for the iteration-order contract.
278+
*/
279+
export function _firstEntry<T extends AnyObject>(obj: T): [keyof T, ValueOf<T>] | undefined {
280+
for (const k in obj) {
281+
if (Object.hasOwn(obj, k)) return [k as keyof T, obj[k] as ValueOf<T>]
282+
}
283+
return undefined
284+
}
285+
245286
export function _objectNullValuesToUndefined<T extends AnyObject>(
246287
obj: T,
247288
opt: MutateOptions = {},

0 commit comments

Comments
 (0)