Skip to content
Merged
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Unreleased (Proposed 0.17.0)
# Unreleased (Proposed 0.20.0)

## Changelog

Expand All @@ -21,6 +21,11 @@
- Uppercase letters (word boundaries) are detected for the Latin, Latin-1, Greek (including the accented capitals, eg. `"ώραΈναρξης"` -> `"ώρα-έναρξης"`) and Cyrillic ranges in both their precomposed and decomposed (NFC / NFD) forms, other cased letters (eg. `"Ł"`, `"Č"`) are retained and case converted but don't start a new word (`"łódźŁódź"` -> `"łódźłódź"`), while a decomposed letter whose base letter is within those ranges does (`"C"` followed by a combining caron), so normalize values that may contain both forms first. The same fixed ranges are used on every runtime (no Unicode property escapes, which require ES2018), so the output does not depend on the runtime
- `strLetterCase` and `strCapitalizeWords` now also capitalize a word that follows repeated `_` characters, eg. `"__init__"` -> `"__Init__"` (was `"__init__"`), consistent with `"_init"` -> `"_Init"`
- Unchanged, so the conversions remain reversible: `strCamelCase` only changes the first letter of each word and retains the case of the other letters (`"XMLHttpRequest"` -> `"xMLHttpRequest"`, `strCamelCase("XMLHttpRequest", true)` -> `"XMLHttpRequest"`), `strKebabCase` and `strSnakeCase` remove leading and trailing whitespace but retain any other leading or trailing separators as a single separator (`"_hello_darkness"` <-> `"-hello-darkness"`), and `strCapitalizeWords` only splits words at separators (`"fooBarBaz"` -> `"Foobarbaz"`, now documented)
- The exported types compile with TypeScript v3.4 again, as documented (they had stopped compiling from v0.14.0, with some earlier issues)
- `DeepPartial`, `DeepReadonly` and `DeepRequired` now resolve arrays through the new `DeepPartialArray`, `DeepReadonlyArray` and `DeepRequiredArray` interfaces, the resulting types are unchanged
- `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)

### Performance Improvements

Expand All @@ -29,6 +34,8 @@
- `fnCall()` is now an alias of the intrinsic `Function.prototype.call` bound to itself (`FuncCall.bind(FuncCall)`, so `fnCall(fn, thisArg, ...args)` is `fn.call(thisArg, ...args)`) rather than a wrapper function that sliced the arguments into a new array and called `fn.apply()`. This is ~1.8x (polymorphic call sites) to ~30x (monomorphic) faster per call in V8 micro-benchmarks and smaller, which also benefits the helpers that call it per item (eg. `arrDropWhile()`, `arrFlatMap()` and the iterator helpers). It also no longer depends on the function's own (overridable) `call` / `apply` properties, and as it's now a bound function `fnCall.name` is `"bound call"` and `fnCall.length` is `1`
- `fnApply()` is now an alias of the intrinsic `Function.prototype.call` bound to the intrinsic `Function.prototype.apply` (`FuncCall.bind(FuncApply)`, so `fnApply(fn, thisArg, argArray)` is `fn.apply(thisArg, argArray)`) rather than a wrapper function calling `fn.apply()`, which is smaller with the same or slightly better performance (within ~10% in V8 micro-benchmarks), used by the timer, cache, lazy, `arrAppend()`, `createFnDeferredProxy()` and custom error helpers. It also no longer depends on the function's own (overridable) `call` / `apply` properties, and `fnApply.name` is `"bound call"` and `fnApply.length` is `1`
- `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)

# v0.16.0 July 18th, 2026

Expand Down
4 changes: 2 additions & 2 deletions lib/src/array/arrIndexOfSubset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function _normalizeFromIndex(value: number | null | undefined, defValue: number)
* of `theArray` is still considered a match against its available prefix. This makes it possible
* to match a shorter (eg. truncated) array against a longer one, or vice-versa.
* @function
* @since 0.17.0
* @since 0.20.0
* @group Array
* @group ArrayLike
* @typeParam T - Identifies the type of array elements
Expand Down Expand Up @@ -122,7 +122,7 @@ export function arrIndexOfSubset<T>(theArray: ArrayLike<T>, subset: ArrayLike<T>
* elements -- so a `subset` that "runs off the end" of `theArray` is still considered a
* match against its available prefix.
* @function
* @since 0.17.0
* @since 0.20.0
* @group Array
* @group ArrayLike
* @typeParam T - Identifies the type of array elements
Expand Down
15 changes: 10 additions & 5 deletions 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 { ArrSlice, CALL, NULL_VALUE } from "../internal/constants";
import { ArrProto } from "../internal/constants";
import { _unwrapFunction } from "../internal/unwrapFunction";

/**
* The arrSlice() method returns a shallow copy of a portion of an array into a new array object
Expand All @@ -24,9 +25,14 @@ import { ArrSlice, CALL, NULL_VALUE } from "../internal/constants";
*
* For both start and end, a negative index can be used to indicate an offset from the end of the array.
* For example, -2 refers to the second to last element of the array.
*
* When the value has its own `slice` function it is used (like `theArray.slice(start, end)`), otherwise
* `Array.prototype.slice` is called with the value as `this`.
* @function
* @since 0.9.3
* @group Array
* @group ArrayLike
* @param theArray - The array or array-like value to extract the elements from.
* @param start - Zero-based index at which to start extraction, converted to an integer.
* - Negative index counts back from the end of the array — if start \< 0, start + array.length is used.
* - If start \< -array.length or start is omitted, 0 is used.
Expand All @@ -38,19 +44,18 @@ import { ArrSlice, CALL, NULL_VALUE } from "../internal/constants";
* - If end \>= array.length or end is omitted, array.length is used, causing all elements until the
* end to be extracted.
* - If end is positioned before or at start after normalization, nothing is extracted.
* @returns A new array containing the extracted elements.
* @example
* ```ts
* const lyrics = ["Hello", "Darkness", "my", "old", "friend.", "I've", "come", "to", "talk" ];
*
* arrSlice(lyrics); // [ "Hello", "Darkness", "my", "old", "friend.", "I've", "come", "to", "talk" ]
* arrSlice(lyrics, 1, 3); // [ "Darkness", "my" ]
* arrSlicw(lyrics, 2); // [ "my", "old", "friend.", "I've", "come", "to", "talk" ]
* arrSlice(lyrics, 2); // [ "my", "old", "friend.", "I've", "come", "to", "talk" ]
* arrSlice(lyrics, 2, 4); // [ "my", "old" ]
* arrSlice(lyrics, 1, 5); // [ "Darkness", "my", "old", "friend." ]
* arrSlice(lyrics, -2); // [ "to", "talk" ]
* arrSlice(lyrics, 2, -1); // [ "my", "old", "friend.", "I've", "come", "to" ]
* ```
*/
export function arrSlice<T>(theArray: ArrayLike<T>, start?: number, end?: number): T[] {
return ((theArray ? (theArray as any)["slice"] : NULL_VALUE) || ArrSlice).apply(theArray, ArrSlice[CALL](arguments, 1));
}
export const arrSlice: <T>(theArray: ArrayLike<T>, start?: number, end?: number) => T[] = (/*#__PURE__*/_unwrapFunction("slice", ArrProto));
7 changes: 4 additions & 3 deletions lib/src/funcs/fnProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import { arrForEach } from "../array/forEach";
import { isArray, isFunction } from "../helpers/base";
import { ArrSlice, CALL } from "../internal/constants";
import { fnApply, fnBind } from "./funcs";
import { ProxyFunctionDef, TypeFuncNames } from "./types";

Expand Down Expand Up @@ -65,8 +64,10 @@ import { ProxyFunctionDef, TypeFuncNames } from "./types";
export function createFnDeferredProxy<H, F extends (...args:any) => any>(hostFn: () => H, funcName: TypeFuncNames<H>): F {

return function() {
// Capture the original arguments passed to the method
var theArgs = ArrSlice[CALL](arguments);
// Capture the original arguments passed to the method, these are passed directly to fnApply rather than
// being copied, as (ES5+) each call has its own arguments object, this function has no named parameters
// (so there are no mapped parameters in non-strict mode) and they are only used during this call
var theArgs = arguments;
let theHost = hostFn();

return fnApply(theHost[funcName] as (...args: any) => any, theHost, theArgs);
Expand Down
3 changes: 2 additions & 1 deletion lib/src/helpers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ArrCls, FUNCTION, LENGTH, NULL_VALUE, OBJECT, ObjProto, TO_STRING, UNDE
import { _isPolyfillType } from "../internal/poly_helpers";
import { _pureRef } from "../internal/treeshake_helpers";
import { safeGet } from "./safe_get";
import { IAsyncGeneratorFunction } from "./types";

let _primitiveTypes: string[];

Expand Down Expand Up @@ -502,7 +503,7 @@ export const isGenerator: (value: any) => value is GeneratorFunction = (/*#__PUR
* isAsyncGenerator(undefined); // false
* ```
*/
export const isAsyncGenerator: (value: any) => value is AsyncGeneratorFunction = (/*#__PURE__*/_createObjIs<AsyncGeneratorFunction>("AsyncGeneratorFunction"));
export const isAsyncGenerator: (value: any) => value is IAsyncGeneratorFunction = (/*#__PURE__*/_createObjIs<IAsyncGeneratorFunction>("AsyncGeneratorFunction"));

/**
* Checks to see if the past value is an object value
Expand Down
26 changes: 2 additions & 24 deletions lib/src/helpers/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,9 @@
import { fnApply } from "../funcs/funcs";
import { NULL_VALUE } from "../internal/constants";
import { objDefineProp } from "../object/define";
import { ICachedValue } from "./types";

/**
* A generic interface for holding a cached value
* @since 0.10.5
* @group Helpers
* @group Cache
* @typeParam T - The type of the value to be cached
* @example
* ```ts
* let cachedValue: ICachedValue<string> = {
* v: "some value"
* };
* ```
*/
export interface ICachedValue<T> {
/**
* Returns the current cached value
*/
v: T,

/**
* Returns the current cached value
*/
toJSON(): T;
}
export { ICachedValue };

/**
* Create and return a readonly {@link ICachedValue} instance that is populated with the provided value.
Expand Down
6 changes: 4 additions & 2 deletions lib/src/helpers/customError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { fnApply, fnCall } from "../funcs/funcs";
import { ArrSlice, CALL, CONSTRUCTOR, NAME, NULL_VALUE, PROTOTYPE } from "../internal/constants";
import { CONSTRUCTOR, NAME, NULL_VALUE, PROTOTYPE } from "../internal/constants";
import { objCreate } from "../object/create";
import { objDefine } from "../object/define";
import { objGetPrototypeOf } from "../object/object";
Expand Down Expand Up @@ -177,7 +177,9 @@ export function createCustomError<T extends ErrorConstructor = CustomErrorConstr

try {
safe(_setName, [theBaseClass, name]);
let _self = fnApply(theBaseClass, _this, superArgsFn ? superArgsFn(theArgs) : ArrSlice[CALL](theArgs)) || _this;
// The arguments object is passed directly to the base class (apply accepts any array-like) rather than
// being sliced into a new array, ctorFn has no named parameters and the arguments are not modified
let _self = fnApply(theBaseClass, _this, superArgsFn ? superArgsFn(theArgs) : theArgs) || _this;
if (_self !== _this) {
// Looks like runtime error constructor reset the prototype chain, so restore it
let orgProto = objGetPrototypeOf(_this);
Expand Down
119 changes: 119 additions & 0 deletions lib/src/helpers/deep_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* @nevware21/ts-utils
* https://github.com/nevware21/ts-utils
*
* Copyright (c) 2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

/**
* Represents a recursive partial type where all nested properties are optional.
* @since 0.14.0
* @group Helpers
* @typeParam T - The type to transform.
* @example
* ```ts
* interface User {
* name: string;
* address: {
* street: string;
* city: string;
* };
* }
*
* // All nested properties become optional
* function updateUser(id: number, patch: DeepPartial<User>) { }
*
* updateUser(1, { address: { city: "Seattle" } }); // OK — street is not required
* ```
*/
export type DeepPartial<T> = T extends Function ? T :
T extends ReadonlyArray<infer U> ? DeepPartialArray<U> :
T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;

/**
* The readonly array produced by {@link DeepPartial} for an array of `T`, this is the same as
* `ReadonlyArray<DeepPartial<T>>`. It is an interface as an interface defers resolving its base type,
* while TypeScript v3.6 and earlier cannot resolve a type alias which directly references itself as a type argument.
* @since 0.20.0
* @group Helpers
* @typeParam T - The element type of the original array
*/
export interface DeepPartialArray<T> extends ReadonlyArray<DeepPartial<T>> {
}

/**
* Represents a recursive readonly type where all nested properties are readonly.
* @since 0.14.0
* @group Helpers
* @typeParam T - The type to transform.
* @example
* ```ts
* interface Config {
* server: {
* host: string;
* port: number;
* };
* }
*
* const cfg: DeepReadonly<Config> = { server: { host: "localhost", port: 8080 } };
*
* // cfg.server.host = "example.com"; // Error: cannot assign to a read-only property
* ```
*/
export type DeepReadonly<T> = T extends Function ? T :
T extends ReadonlyArray<infer U> ? DeepReadonlyArray<U> :
T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> } : T;

/**
* The readonly array produced by {@link DeepReadonly} for an array of `T`, this is the same as
* `ReadonlyArray<DeepReadonly<T>>`. It is an interface as an interface defers resolving its base type,
* while TypeScript v3.6 and earlier cannot resolve a type alias which directly references itself as a type argument.
* @since 0.20.0
* @group Helpers
* @typeParam T - The element type of the original array
*/
export interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {
}

/**
* Represents a recursive required type where all nested properties are required.
* @since 0.14.0
* @group Helpers
* @typeParam T - The type to transform.
* @example
* ```ts
* interface Options {
* timeout?: number;
* retry?: {
* count?: number;
* delay?: number;
* };
* }
*
* // After applying defaults every field is guaranteed to be present
* function withDefaults(opts: Options): DeepRequired<Options> {
* return {
* timeout: opts.timeout ?? 5000,
* retry: {
* count: opts.retry?.count ?? 3,
* delay: opts.retry?.delay ?? 1000
* }
* };
* }
* ```
*/
export type DeepRequired<T> = T extends Function ? T :
T extends ReadonlyArray<infer U> ? DeepRequiredArray<U> :
T extends object ? { [P in keyof T]-?: DeepRequired<NonNullable<T[P]>> } : NonNullable<T>;

/**
* The readonly array produced by {@link DeepRequired} for an array of `T`, this is the same as
* `ReadonlyArray<DeepRequired<NonNullable<T>>>`. It is an interface as an interface defers resolving its base type,
* while TypeScript v3.6 and earlier cannot resolve a type alias which directly references itself as a type argument.
* @since 0.20.0
* @group Helpers
* @typeParam T - The element type of the original array
*/
export interface DeepRequiredArray<T> extends ReadonlyArray<DeepRequired<NonNullable<T>>> {
}
11 changes: 7 additions & 4 deletions lib/src/helpers/extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
*/

import { arrForEach } from "../array/forEach";
import { ArrSlice, CALL } from "../internal/constants";
import { objCopyProps, objDeepCopy } from "../object/copy";

/**
* @internal
* @ignore
* Copies the properties of each of the arguments onto the target. The callers pass their arguments object
* directly rather than a copy, as (ES5+) each call has its own arguments object, the callers never reassign
* their named parameters (which would change a mapped arguments object in non-strict mode) and the arguments
* are only read during the call.
*/
function _doExtend<T>(target: T, theArgs: any[]): any {
function _doExtend<T>(target: T, theArgs: ArrayLike<any>): any {
arrForEach(theArgs, (theArg) => {
objCopyProps(target, theArg);
});
Expand All @@ -41,7 +44,7 @@ export function deepExtend<T>(target: T, ...theArgs: any): T & any;
* @returns - A new object or the original
*/
export function deepExtend<T, T1, T2, T3, T4, T5, T6>(target: T, obj1?: T1, obj2?: T2, obj3?: T3, obj4?: T4, obj5?: T5, obj6?: T6): T & T1 & T2 & T3 & T4 & T5 & T6 {
return _doExtend(objDeepCopy(target) || {}, ArrSlice[CALL](arguments));
return _doExtend(objDeepCopy(target) || {}, arguments);
}

/**
Expand All @@ -61,7 +64,7 @@ export function objExtend<T>(target: T, ...theArgs: any): T & any;
* @returns - A new object or the original
*/
export function objExtend<T, T1, T2, T3, T4, T5, T6>(target: T, obj1?: T1, obj2?: T2, obj3?: T3, obj4?: T4, obj5?: T5, obj6?: T6): T & T1 & T2 & T3 & T4 & T5 & T6 {
return _doExtend(target || {}, ArrSlice[CALL](arguments));
return _doExtend(target || {}, arguments);
}


22 changes: 3 additions & 19 deletions lib/src/helpers/lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import { fnApply } from "../funcs/funcs";
import { _GlobalTestHooks, _getGlobalConfig } from "../internal/global";
import { objDefineProp } from "../object/define";
import { ICachedValue } from "./cache";
import { ILazyValue } from "./types";

export { ILazyValue };

/**
* @internal
Expand All @@ -21,24 +23,6 @@ export function _initTestHooks() {
_globalLazyTestHooks = _getGlobalConfig();
}

/**
* Interface of the object returned by the {@link getLazy} instance
* @since 0.4.5
* @group Lazy
*/
export interface ILazyValue<T> extends ICachedValue<T> {
/**
* Returns the current cached value from the lazy lookup, if the callback function has not yet occurred
* accessing the value will cause the lazy evaluation to occur and the result will be returned.
*/
v: T,

/**
* Identifies if this instance is bypassing the internal caching mechanism which is used for testing
*/
b?: boolean
}

/**
* Create and return an readonly {@link ILazyValue} instance which will cache and return the value returned
* by the callback function. The callback function will only be called once, multiple access of the value
Expand Down
Loading
Loading