Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,42 @@
{
"name": "es5-full",
"path": "lib/dist/es5/mod/ts-utils.js",
"limit": "39 kb",
"limit": "40.5 kb",
"brotli": false,
"running": false
},
{
"name": "es6-full",
"path": "lib/dist/es6/mod/ts-utils.js",
"limit": "37.5 kb",
"limit": "39 kb",
"brotli": false,
"running": false
},
{
"name": "es5-full-brotli",
"path": "lib/dist/es5/mod/ts-utils.js",
"limit": "13.25 kb",
"limit": "13.75 kb",
"brotli": true,
"running": false
},
{
"name": "es6-full-brotli",
"path": "lib/dist/es6/mod/ts-utils.js",
"limit": "13.25 kb",
"limit": "13.75 kb",
"brotli": true,
"running": false
},
{
"name": "es5-zip",
"path": "lib/dist/es5/mod/ts-utils.js",
"limit": "14.5 Kb",
"limit": "15.25 Kb",
"gzip": true,
"running": false
},
{
"name": "es6-zip",
"path": "lib/dist/es6/mod/ts-utils.js",
"limit": "14.5 Kb",
"limit": "15.25 Kb",
"gzip": true,
"running": false
},
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@
- `ObjDefinePropDescriptorMap` is now `Record<PropertyKey, ObjDefinePropDescriptor>` (unchanged on TypeScript v4.4 and later)
- `isAsyncGenerator` now narrows to the new `IAsyncGeneratorFunction` (assignable to `AsyncGeneratorFunction`), and `RequestIdleCallback` / `scheduleIdleCallback` use the new `IdleCallbackFn`, `IIdleDeadline` and `IIdleRequestOptions` (the same shapes as the DOM types) as these are not provided by older TypeScript versions
- `isAsyncIterable` now narrows to the new `IAsyncIterable` (and `IAsyncIterator`) so the exported types no longer require the ES2018 lib. With TypeScript v4.3 and later it is interchangeable with the native `AsyncIterable`, older compilers using the ES2018 lib see `any` (so a value declared as a union with `AsyncIterable` is no longer narrowed on those versions)
- `arrUnique` and `arrUnion` no longer discard distinct values. Objects, arrays, functions and dates were compared by their string form, so `arrUnique([{ a: 1 }, { b: 2 }])` returned `[{ a: 1 }]` and a `Symbol` threw a `TypeError`; these are now compared by identity, as documented
- `encodeAsBase64`, `decodeBase64` and the URL-safe variants now use the native `btoa` / `atob` in browsers. The native functions were called with the wrong `this` so every call failed and used the polyfill, which gave different results to Node, eg. `decodeBase64("QQ")` returned `"A\0\0"` (now `"A"`) and values containing a line break were corrupted
- `encodeAsHex`, `encodeAsBase64` and `encodeAsBase64Url` no longer corrupt text containing characters above `U+00FF`, and it is restored by the matching decode function
- **Output change:** only for text which was previously corrupted, this is now encoded as the UTF-8 byte order mark followed by the UTF-8 bytes, eg. `encodeAsHex("€")` -> `"efbbbfe282ac"` (was `"ac"`). The encoding of all other text is unchanged, except for text that itself starts with the characters of that marker
- **Decoding change:** `decodeHex`, `decodeBase64` and `decodeBase64Url` decode a value that starts with the marker and contains valid UTF-8 as text, all other values are decoded as before
- An unpaired surrogate is not valid text, so like `TextEncoder` it is encoded as `U+FFFD`
- `objCopyProps` and `objExtend` now refuse to change a built-in prototype such as `Object.prototype` (the prototype pollution guard was checking the source rather than the target), and their documentation now states that only own enumerable properties are copied and that `__proto__`, `constructor` and `prototype` are skipped
- `arrForEach` no longer throws when passed a string, which also fixes `arrChunk`, `arrCompact`, `arrUnique`, `arrGroupBy`, `arrPartition`, `arrDropWhile`, `arrTakeWhile`, `arrIntersection`, `arrDifference` and `arrFlatten`, and `arrShuffle` and the `polyArrWith` polyfill also now support a string. `arrDropWhile`, `arrDrop`, `arrTake` and `arrRotate` now return an array when passed a string (as documented), previously they could return a string
- `polyArrFindLastIndex` and `polyArrFindLast` (used by `arrFindLastIndex` / `arrFindLast` when there is no native function) now visit every index like the native functions and as documented, previously the empty slots of a sparse array were skipped and a string threw a `TypeError`
- `iterTake` now closes the source iterator on the `next()` following the last value (like the native `Iterator.prototype.take`), previously it was closed before the consumer received the last value
- `arrIntersection` and `arrUnion` no longer throw for a `null` or `undefined` array after the first, `arrIntersection` treats it as empty and `arrUnion` ignores it
- `scheduleMicrotask` and `scheduleNextTick` no longer report `enabled` for a task that was rejected by, or run synchronously by, a custom `scheduleFn`
- `polyArrAt`, `polyArrWith` and `polyArrFill` now convert their indexes to integers like the native functions, so fractional and `NaN` indexes no longer return the wrong result or add fractional properties
- `objDeepCopy` handlers now receive the correct `path` for an empty string key and for a key of `0` passed to `details.copy()`

### Performance Improvements

Expand All @@ -36,6 +50,10 @@
- `fnBind()` now forwards the arguments object to the intrinsic `Function.prototype.bind` (`FuncCall.apply(FuncBind, arguments)`, which is `fn.bind(thisArg, ...args)`) rather than slicing the arguments into a new array and calling `fn.bind.apply()`, ~60-69% faster per call in V8 micro-benchmarks. It remains a function so the existing overloads are unchanged, and it no longer depends on the function's own (overridable) `bind` / `call` / `apply` properties
- `arrSlice()` now uses the same native function wrapper as `arrIndexOf()` / `arrMap()` (`_unwrapFunction("slice", ArrProto)`) rather than slicing its own arguments into a new array before calling `slice`, ~16% faster in a V8 micro-benchmark (and smaller). It still uses the value's own `slice` function when present, otherwise `Array.prototype.slice`, and as it's now a `const` its `name` and `length` have changed
- `createFnDeferredProxy()`, `objExtend()`, `deepExtend()`, `createCustomError()` (when calling the base class) and the `scheduleTimeout()` / `createTimeout()` callback wrapper now pass their `arguments` object directly rather than slicing it into a new array on every call (`objExtend()` ~29% faster in a V8 micro-benchmark). The arguments are still copied where they are kept or modified after the call returns (the timer `refresh()` arguments)
- `objCopyProps`, `objExtend`, `deepExtend` and `objDeepCopy` no longer allocate a deep copy context for primitive values when there is no handler, ~80% faster for objects of primitive values
- `objDeepCopy` and `objCopyProps` now track visited objects with a `Map` when available (a linear search otherwise), so they no longer slow down quadratically as the number of objects grows, eg. copying 16,000 objects went from ~585ms to ~48ms
- `arrUnique` and `arrUnion` now use a `Map` when available (a linear search otherwise) for all values rather than building a string key for primitives, ~5x faster for numbers and strings
- `encodeAsBase64` and `decodeBase64` are ~10x faster in browsers now the native functions are used, as they no longer throw and fall back to the polyfill on every call

# v0.16.0 July 18th, 2026

Expand Down
17 changes: 7 additions & 10 deletions lib/src/array/at.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,13 @@ export function polyArrAt<T>(theArray: ArrayLike<T>, index: number): T | undefin

if (isArrayLike(theArray)) {
const len = theArray.length;
let idx = index;

// Convert negative index to positive
if (idx < 0) {
idx = len + idx;
}

// Check bounds and get value
if (idx >= 0 && idx < len) {
result = theArray[idx];
// Convert the index (NaN becomes 0), any other value outside of the range is rejected by the check
let idx = +index || 0;

// Check the bounds before truncating, which uses % as a bitwise operator would wrap the indexes of an array longer than 2^31
if (len && idx > -len - 1 && idx < len) {
idx -= idx % 1;
result = theArray[idx < 0 ? len + idx : idx];
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/src/array/drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT license.
*/

import { arrSlice } from "./slice";
import { _arrSliceToArr } from "./slice";
import { mathMax } from "../math/min_max";

/**
Expand All @@ -33,5 +33,5 @@ import { mathMax } from "../math/min_max";
*/
/*#__NO_SIDE_EFFECTS__*/
export function arrDrop<T>(theArray: ArrayLike<T> | null | undefined, count: number): T[] {
return arrSlice(theArray, mathMax(0, count));
return _arrSliceToArr(theArray, mathMax(0, count));
}
4 changes: 2 additions & 2 deletions lib/src/array/drop_while.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { fnCall } from "../funcs/funcs";
import { isArrayLike } from "../helpers/base";
import { ArrPredicateCallbackFn, ArrPredicateCallbackFn2 } from "./callbacks";
import { arrForEach } from "./forEach";
import { arrSlice } from "./slice";
import { _arrSliceToArr } from "./slice";

/**
* The arrDropWhile() method returns a new array with elements from the beginning removed
Expand Down Expand Up @@ -47,7 +47,7 @@ export function arrDropWhile<T, E extends T = T>(
if (isArrayLike(theArray)) {
arrForEach(theArray, (item, index) => {
if (!fnCall(callbackFn, thisArg, item, index, theArray as any)) {
result = arrSlice(theArray, index);
result = _arrSliceToArr(theArray, index);
return -1; // Break out of forEach
}
});
Expand Down
44 changes: 28 additions & 16 deletions lib/src/array/fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,32 @@ import { _unwrapFunctionWithPoly } from "../internal/unwrapFunction";
import { isArrayLike } from "../helpers/base";
import { getLength } from "../helpers/length";
import { WritableArrayLike } from "../helpers/arrayLike";
import { mathMax, mathMin } from "../math/min_max";

/**
* @internal
* @ignore
* Converts a start / end value into the position within the array that it identifies, which is always in range.
* @param value - The value to convert (NaN becomes 0)
* @param len - The length of the array
* @returns The position within the array
*/
/*#__NO_SIDE_EFFECTS__*/
function _getFillIdx(value: number, len: number): number {
let idx = +value || 0;

// Check the bounds before truncating, which uses % as a bitwise operator would wrap the indexes of an array longer than 2^31
if (idx <= -len) {
return 0;
}

if (idx >= len) {
return len;
}

idx -= idx % 1;

return idx < 0 ? len + idx : idx;
}

/**
* The arrFill() method changes all elements in an array to a static value, from a start index
Expand Down Expand Up @@ -55,21 +80,8 @@ export const arrFill = (/*#__PURE__*/_unwrapFunctionWithPoly("fill", ArrProto, p
export function polyArrFill<T>(theArray: WritableArrayLike<T>, value: T, start?: number, end?: number): WritableArrayLike<T> {
if (isArrayLike(theArray)) {
const len = getLength(theArray);
let startIdx = start === UNDEF_VALUE ? 0 : start;
let endIdx = end === UNDEF_VALUE ? len : end;

// Handle negative indices
if (startIdx < 0) {
startIdx = mathMax(len + startIdx, 0);
} else {
startIdx = mathMin(startIdx, len);
}

if (endIdx < 0) {
endIdx = mathMax(len + endIdx, 0);
} else {
endIdx = mathMin(endIdx, len);
}
const startIdx = _getFillIdx(start, len);
const endIdx = end === UNDEF_VALUE ? len : _getFillIdx(end, len);

// Fill the array
for (let i = startIdx; i < endIdx; i++) {
Expand Down
8 changes: 5 additions & 3 deletions lib/src/array/forEach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* Licensed under the MIT license.
*/

import { isStrictNullOrUndefined } from "../helpers/base";
import { CALL, LENGTH } from "../internal/constants";
import { isStrictNullOrUndefined, isString } from "../helpers/base";
import { CALL, LENGTH, ObjClass } from "../internal/constants";

/**
* Calls the provided `callbackFn` function once for each element in an array in ascending index order. It is not invoked for index properties
Expand Down Expand Up @@ -56,8 +56,10 @@ export function arrForEach<T = any>(theArray: ArrayLike<T>, callbackfn: (value:
export function arrForEach<T = any, A extends ArrayLike<T> = ArrayLike<T>>(theArray: A, callbackfn: (value: T, index: number, array: A) => void | number, thisArg?: any): void {
if (theArray) {
const len = theArray[LENGTH] >>> 0;
// The "in" operator throws for a string primitive (which is ArrayLike), so use its object form
const theObj: any = isString(theArray) ? ObjClass(theArray) : theArray;
for (let idx = 0; idx < len; idx++) {
if (idx in theArray) {
if (idx in theObj) {
Comment on lines +59 to +62
if (callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theArray as any : thisArg, theArray[idx], idx, theArray) === -1) {
break;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/array/intersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function arrIntersection<T>(): T[] {
arrForEach(firstArray, (item) => {
let inAll = true;
arrForEach(theArrays, (arr, index) => {
if (index > 0 && !arrIncludes(arr, item)) {
if (index > 0 && !(isArrayLike(arr) && arrIncludes(arr, item))) {
inAll = false;
return -1; // Break out of forEach
}
Expand Down
4 changes: 2 additions & 2 deletions lib/src/array/rotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT license.
*/

import { arrSlice } from "./slice";
import { _arrSliceToArr, arrSlice } from "./slice";
import { isArrayLike } from "../helpers/base";
import { arrAppend } from "./append";

Expand Down Expand Up @@ -60,7 +60,7 @@ export function arrRotate<T>(theArray: ArrayLike<T> | null | undefined, count: n
}

if (!result) {
result = arrSlice(theArray);
result = _arrSliceToArr(theArray);
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/src/array/shuffle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { mathFloor } from "../math/floor";
import { mathRandom } from "../math/random";
import { getLength } from "../helpers/length";
import { arrSlice } from "./slice";
import { _arrSliceToArr } from "./slice";

/**
* The arrShuffle() method returns a new array with elements randomly shuffled.
Expand All @@ -33,7 +33,7 @@ import { arrSlice } from "./slice";
*/
/*#__NO_SIDE_EFFECTS__*/
export function arrShuffle<T>(theArray: ArrayLike<T> | null | undefined): T[] {
const result = arrSlice(theArray);
const result: T[] = _arrSliceToArr(theArray);
const len = getLength(result);

// Fisher-Yates shuffle
Expand Down
18 changes: 17 additions & 1 deletion lib/src/array/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* Licensed under the MIT license.
*/

import { ArrProto } from "../internal/constants";
import { isString } from "../helpers/base";
import { ArrProto, ArrSlice, CALL } from "../internal/constants";
import { _unwrapFunction } from "../internal/unwrapFunction";

/**
Expand Down Expand Up @@ -59,3 +60,18 @@ import { _unwrapFunction } from "../internal/unwrapFunction";
* ```
*/
export const arrSlice: <T>(theArray: ArrayLike<T>, start?: number, end?: number) => T[] = (/*#__PURE__*/_unwrapFunction("slice", ArrProto));

/**
* @internal
* @ignore
* Slices the value into an array. A string has its own slice function which returns a string, so it uses the Array
* slice function, any other value uses {@link arrSlice} (so it still uses the value's own slice function when present).
* @param theArray - The value to slice
* @param start - The zero-based index at which to start extraction
* @param end - The zero-based index at which to end extraction
* @returns The new array
*/
/*#__NO_SIDE_EFFECTS__*/
export function _arrSliceToArr<T>(theArray: ArrayLike<T> | null | undefined, start?: number, end?: number): T[] {
return isString(theArray) ? ArrSlice[CALL](theArray, start, end) : arrSlice(theArray, start, end);
}
4 changes: 2 additions & 2 deletions lib/src/array/take.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT license.
*/

import { arrSlice } from "./slice";
import { _arrSliceToArr } from "./slice";
import { mathMax } from "../math/min_max";

/**
Expand All @@ -33,5 +33,5 @@ import { mathMax } from "../math/min_max";
*/
/*#__NO_SIDE_EFFECTS__*/
export function arrTake<T>(theArray: ArrayLike<T> | null | undefined, count: number): T[] {
return arrSlice(theArray, 0, mathMax(0, count));
return _arrSliceToArr(theArray, 0, mathMax(0, count));
}
5 changes: 4 additions & 1 deletion lib/src/array/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Licensed under the MIT license.
*/

import { isArrayLike } from "../helpers/base";
import { arrUnique } from "./unique";
import { arrAppend } from "./append";
import { arrSlice } from "./slice";
Expand Down Expand Up @@ -39,7 +40,9 @@ export function arrUnion<T>(): T[] {
let theArgs = arguments;

arrForEach(theArgs, (arr) => {
combined = arrAppend(combined, arrSlice(arr));
if (isArrayLike(arr)) {
combined = arrAppend(combined, arrSlice(arr));
}
});

return arrUnique(combined);
Expand Down
12 changes: 3 additions & 9 deletions lib/src/array/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
*/

import { isArrayLike } from "../helpers/base";
import { objCreate } from "../object/create";
import { objHasOwn } from "../object/has_own";
import { _createIdentityMap } from "../internal/identity_map";
import { arrForEach } from "./forEach";

/**
Expand Down Expand Up @@ -38,15 +37,10 @@ export function arrUnique<T>(theArray: ArrayLike<T> | null | undefined): T[] {
const result: T[] = [];

if (isArrayLike(theArray)) {
const seen: any = objCreate(null);
const seen = _createIdentityMap<number>();

arrForEach(theArray, (item) => {
const key = ((typeof item) + "_" + item);

if (!objHasOwn(seen, key)) {
seen[key] = 1;
result.push(item);
}
!seen(item, 1) && result.push(item);
});
}

Expand Down
21 changes: 12 additions & 9 deletions lib/src/array/with.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ArrProto } from "../internal/constants";
import { _unwrapFunctionWithPoly } from "../internal/unwrapFunction";
import { isArrayLike } from "../helpers/base";
import { getLength } from "../helpers/length";
import { arrSlice } from "./slice";
import { _arrSliceToArr } from "./slice";
import { throwRangeError } from "../helpers/throw";

/**
Expand Down Expand Up @@ -65,20 +65,23 @@ export function polyArrWith<T>(theArray: ArrayLike<T>, index: number, value: T):
}

const len = getLength(theArray);
let idx = index;

// Convert the index (NaN becomes 0), any other value outside of the range is rejected by the check
let idx = +index || 0;

// Check the bounds before truncating, which uses % as a bitwise operator would wrap the indexes of an array longer than 2^31
if (!len || idx <= -len - 1 || idx >= len) {
throwRangeError("Index out of bounds");
}

idx -= idx % 1;

// Convert negative index to positive
if (idx < 0) {
idx = len + idx;
}

// Check bounds
if (idx < 0 || idx >= len) {
throwRangeError("Index out of bounds");
}

// Create a copy and set value
result = arrSlice(theArray);
result = _arrSliceToArr(theArray);
result[idx] = value;

return result;
Expand Down
Loading
Loading