diff --git a/CHANGELOG.md b/CHANGELOG.md index 638591b5..ed887e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased (Proposed 0.17.0) +# Unreleased (Proposed 0.20.0) ## Changelog @@ -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` (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 @@ -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 diff --git a/lib/src/array/arrIndexOfSubset.ts b/lib/src/array/arrIndexOfSubset.ts index ddeda994..504cb30e 100644 --- a/lib/src/array/arrIndexOfSubset.ts +++ b/lib/src/array/arrIndexOfSubset.ts @@ -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 @@ -122,7 +122,7 @@ export function arrIndexOfSubset(theArray: ArrayLike, subset: ArrayLike * 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 diff --git a/lib/src/array/slice.ts b/lib/src/array/slice.ts index e0951315..3a70a26a 100644 --- a/lib/src/array/slice.ts +++ b/lib/src/array/slice.ts @@ -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 @@ -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. @@ -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(theArray: ArrayLike, start?: number, end?: number): T[] { - return ((theArray ? (theArray as any)["slice"] : NULL_VALUE) || ArrSlice).apply(theArray, ArrSlice[CALL](arguments, 1)); -} +export const arrSlice: (theArray: ArrayLike, start?: number, end?: number) => T[] = (/*#__PURE__*/_unwrapFunction("slice", ArrProto)); diff --git a/lib/src/funcs/fnProxy.ts b/lib/src/funcs/fnProxy.ts index b326e93e..50dd0a18 100644 --- a/lib/src/funcs/fnProxy.ts +++ b/lib/src/funcs/fnProxy.ts @@ -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"; @@ -65,8 +64,10 @@ import { ProxyFunctionDef, TypeFuncNames } from "./types"; export function createFnDeferredProxy any>(hostFn: () => H, funcName: TypeFuncNames): 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); diff --git a/lib/src/helpers/base.ts b/lib/src/helpers/base.ts index 74e46dfb..74b090f8 100644 --- a/lib/src/helpers/base.ts +++ b/lib/src/helpers/base.ts @@ -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[]; @@ -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")); +export const isAsyncGenerator: (value: any) => value is IAsyncGeneratorFunction = (/*#__PURE__*/_createObjIs("AsyncGeneratorFunction")); /** * Checks to see if the past value is an object value diff --git a/lib/src/helpers/cache.ts b/lib/src/helpers/cache.ts index f4ac1349..4314517a 100644 --- a/lib/src/helpers/cache.ts +++ b/lib/src/helpers/cache.ts @@ -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 = { - * v: "some value" - * }; - * ``` - */ -export interface ICachedValue { - /** - * 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. diff --git a/lib/src/helpers/customError.ts b/lib/src/helpers/customError.ts index 800c2003..ce141a3c 100644 --- a/lib/src/helpers/customError.ts +++ b/lib/src/helpers/customError.ts @@ -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"; @@ -177,7 +177,9 @@ export function createCustomError) { } + * + * updateUser(1, { address: { city: "Seattle" } }); // OK — street is not required + * ``` + */ +export type DeepPartial = T extends Function ? T : + T extends ReadonlyArray ? DeepPartialArray : + T extends object ? { [P in keyof T]?: DeepPartial } : T; + +/** + * The readonly array produced by {@link DeepPartial} for an array of `T`, this is the same as + * `ReadonlyArray>`. 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 extends ReadonlyArray> { +} + +/** + * 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 = { server: { host: "localhost", port: 8080 } }; + * + * // cfg.server.host = "example.com"; // Error: cannot assign to a read-only property + * ``` + */ +export type DeepReadonly = T extends Function ? T : + T extends ReadonlyArray ? DeepReadonlyArray : + T extends object ? { readonly [P in keyof T]: DeepReadonly } : T; + +/** + * The readonly array produced by {@link DeepReadonly} for an array of `T`, this is the same as + * `ReadonlyArray>`. 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 extends ReadonlyArray> { +} + +/** + * 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 { + * return { + * timeout: opts.timeout ?? 5000, + * retry: { + * count: opts.retry?.count ?? 3, + * delay: opts.retry?.delay ?? 1000 + * } + * }; + * } + * ``` + */ +export type DeepRequired = T extends Function ? T : + T extends ReadonlyArray ? DeepRequiredArray : + T extends object ? { [P in keyof T]-?: DeepRequired> } : NonNullable; + +/** + * The readonly array produced by {@link DeepRequired} for an array of `T`, this is the same as + * `ReadonlyArray>>`. 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 extends ReadonlyArray>> { +} diff --git a/lib/src/helpers/extend.ts b/lib/src/helpers/extend.ts index a58eb211..90e1b456 100644 --- a/lib/src/helpers/extend.ts +++ b/lib/src/helpers/extend.ts @@ -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(target: T, theArgs: any[]): any { +function _doExtend(target: T, theArgs: ArrayLike): any { arrForEach(theArgs, (theArg) => { objCopyProps(target, theArg); }); @@ -41,7 +44,7 @@ export function deepExtend(target: T, ...theArgs: any): T & any; * @returns - A new object or the original */ export function deepExtend(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); } /** @@ -61,7 +64,7 @@ export function objExtend(target: T, ...theArgs: any): T & any; * @returns - A new object or the original */ export function objExtend(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); } \ No newline at end of file diff --git a/lib/src/helpers/lazy.ts b/lib/src/helpers/lazy.ts index a5d7bfc7..ec4fa328 100644 --- a/lib/src/helpers/lazy.ts +++ b/lib/src/helpers/lazy.ts @@ -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 @@ -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 extends ICachedValue { - /** - * 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 diff --git a/lib/src/helpers/types.ts b/lib/src/helpers/types.ts index 13719754..c4411eef 100644 --- a/lib/src/helpers/types.ts +++ b/lib/src/helpers/types.ts @@ -24,54 +24,6 @@ */ export type ReadonlyRecord = Readonly>; -/** - * 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) { } - * - * updateUser(1, { address: { city: "Seattle" } }); // OK — street is not required - * ``` - */ -export type DeepPartial = T extends Function ? T : - T extends ReadonlyArray ? ReadonlyArray> : - T extends object ? { [P in keyof T]?: 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 = { server: { host: "localhost", port: 8080 } }; - * - * // cfg.server.host = "example.com"; // Error: cannot assign to a read-only property - * ``` - */ -export type DeepReadonly = T extends Function ? T : - T extends ReadonlyArray ? ReadonlyArray> : - T extends object ? { readonly [P in keyof T]: DeepReadonly } : T; - /** * Represents a writable version of a type by removing readonly from top-level properties. * @since 0.14.0 @@ -96,37 +48,6 @@ export type Mutable = { -readonly [P in keyof T]: T[P] }; -/** - * 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 { - * return { - * timeout: opts.timeout ?? 5000, - * retry: { - * count: opts.retry?.count ?? 3, - * delay: opts.retry?.delay ?? 1000 - * } - * }; - * } - * ``` - */ -export type DeepRequired = T extends Function ? T : - T extends ReadonlyArray ? ReadonlyArray>> : - T extends object ? { [P in keyof T]-?: DeepRequired> } : NonNullable; - /** * Creates a union of all property values of a type. * @since 0.14.0 @@ -189,3 +110,113 @@ export type MicrotaskFn = () => void; * starts dropping tasks or throwing errors, depending on the implementation. */ export type ScheduleMicrotaskFn = (callback: MicrotaskFn, maxQueuedTasks?: number) => void | boolean; + +/** + * 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 = { + * v: "some value" + * }; + * ``` + */ +export interface ICachedValue { + /** + * Returns the current cached value + */ + v: T, + + /** + * Returns the current cached value + */ + toJSON(): T; +} + +/** + * Interface of the object returned by the {@link getLazy} instance + * @since 0.4.5 + * @group Lazy + */ +export interface ILazyValue extends ICachedValue { + /** + * 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 +} + +/** + * The signature of the override timeout function used to create a new timeout instance, it follows the same signature as + * the native `setTimeout` API. + * @since 0.4.4 + * @group Timer + * @param callback - A function to be executed after the timer expires. + * @param ms - The time, in milliseconds that the timer should wait before the specified function or code is executed. + * If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, the next event cycle. + * @param args - Additional arguments which are passed through to the function specified by callback. + * @return The returned timeoutID is a positive integer value which identifies the timer created by the call to setTimeout(). + * This value can be passed to clearTimeout() to cancel the timeout. + */ +export type TimeoutOverrideFn = (callback: (...args: TArgs) => void, ms?: number, ...args: TArgs) => number | any; + +/** + * The signatire of the function to override clearing a previous timeout created with the {@link TimeoutOverrideFn}, it will be passed + * the result returned from the {@link TimeoutOverrideFn} call. + * @since 0.4.5 + * @group Timer + * @param timeoutId - The value returned from the previous {@link TimeoutOverrideFn}. + */ +export type ClearTimeoutOverrideFn = (timeoutId: number | any) => void; + +/** + * Defines the array signature used to pass the override set and clean functions for creating a timeout. + * The first index [0] is the set function to be used and the second index [1] is the clear function to be used. + * @since 0.4.5 + * @group Timer + */ +export type TimeoutOverrideFuncs = [ TimeoutOverrideFn | null, ClearTimeoutOverrideFn | null ]; + +/** + * The signature of an async generator function, this has the same shape as the `AsyncGeneratorFunction` provided by + * TypeScript v3.6 and later, it is defined here so that the exported types can be used with earlier versions. The + * results are `any` so a value of this type can be assigned to an `AsyncGeneratorFunction`. + * @since 0.20.0 + * @group Type Identity + */ +export interface IAsyncGeneratorFunction { + /** + * Creates a new async generator object. + * @param args - The arguments for the async generator function + */ + new (...args: any[]): any; + + /** + * Calls the async generator function and returns the new async generator object. + * @param args - The arguments for the async generator function + */ + (...args: any[]): any; + + /** + * The number of arguments expected by the async generator function. + */ + readonly length: number; + + /** + * The name of the async generator function. + */ + readonly name: string; + + /** + * The prototype of the async generator objects created by the async generator function. + */ + readonly prototype: any; +} diff --git a/lib/src/index.ts b/lib/src/index.ts index f9917d0a..cea65889 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -89,14 +89,17 @@ export { safeGet } from "./helpers/safe_get"; export { safeGetLazy, safeGetWritableLazy, safeGetDeferred, safeGetWritableDeferred } from "./helpers/safe_lazy"; export { throwError, throwTypeError, throwRangeError } from "./helpers/throw"; export { - ReadonlyRecord, DeepPartial, DeepReadonly, Mutable, DeepRequired, ValueOf, NonEmptyArray, - ScheduleMicrotaskFn, MicrotaskFn + ReadonlyRecord, Mutable, ValueOf, NonEmptyArray, + ScheduleMicrotaskFn, MicrotaskFn, IAsyncGeneratorFunction } from "./helpers/types"; +export { + DeepPartial, DeepPartialArray, DeepReadonly, DeepReadonlyArray, DeepRequired, DeepRequiredArray +} from "./helpers/deep_types"; export { hasValue } from "./helpers/value"; export { createArrayIterator } from "./iterator/array"; export { CreateIteratorContext, createIterator, createIterable, createIterableIterator, makeIterable } from "./iterator/create"; export { iterForOf } from "./iterator/forOf"; -export { IterMapCallbackFn, IterPredicateCallbackFn, IterReduceCallbackFn, ArrToMapKeySelectorFn, ArrToMapValueSelectorFn } from "./iterator/types"; +export { IterMapCallbackFn, IterPredicateCallbackFn, IterReduceCallbackFn, ArrToMapKeySelectorFn, ArrToMapValueSelectorFn, IAsyncIterable, IAsyncIterator } from "./iterator/types"; export { iterMap } from "./iterator/iterMap"; export { iterFilter } from "./iterator/iterFilter"; export { iterTake } from "./iterator/iterTake"; @@ -181,7 +184,7 @@ export { WellKnownSymbols } from "./symbol/well_known"; export { ITimerHandler } from "./timer/handler"; export { hasIdleCallback, scheduleIdleCallback, setDefaultIdleTimeout, setDefaultMaxExecutionTime, - getIdleCallback, getCancelIdleCallback, RequestIdleCallback, CancelIdleCallback + getIdleCallback, getCancelIdleCallback, RequestIdleCallback, CancelIdleCallback, IIdleDeadline, IIdleRequestOptions, IdleCallbackFn } from "./timer/idle"; export { scheduleInterval } from "./timer/interval"; export { scheduleMicrotask, MicroTaskOptions, setMicroTaskFallbackOptions } from "./timer/microtask"; diff --git a/lib/src/internal/global.ts b/lib/src/internal/global.ts index bedc4541..51891171 100644 --- a/lib/src/internal/global.ts +++ b/lib/src/internal/global.ts @@ -7,7 +7,7 @@ */ import { safe } from "../helpers/safe"; -import { TimeoutOverrideFuncs } from "../timer/timeout"; +import { TimeoutOverrideFuncs } from "../helpers/types"; import { UNDEFINED } from "./constants"; const GLOBAL_CONFIG_KEY = "__tsUtils$gblCfg"; diff --git a/lib/src/iterator/iterator.ts b/lib/src/iterator/iterator.ts index f5794279..52664189 100644 --- a/lib/src/iterator/iterator.ts +++ b/lib/src/iterator/iterator.ts @@ -9,6 +9,7 @@ import { getKnownSymbol } from "../symbol/symbol"; import { WellKnownSymbols } from "../symbol/well_known"; import { isFunction, isStrictNullOrUndefined } from "../helpers/base"; +import { IAsyncIterable } from "./types"; /** * Checks if the type of value looks like an iterator instance (contains a next function). @@ -72,9 +73,15 @@ export function isIterable(value: any): value is Iterable { * isAsyncIterable(undefined); // false * isAsyncIterable("null"); // false * isAsyncIterable([]); // false + * + * async function* asyncGenerator() { + * yield 1; + * } + * + * isAsyncIterable(asyncGenerator()); // true * ``` */ /*#__NO_SIDE_EFFECTS__*/ -export function isAsyncIterable(value: any): value is AsyncIterable { +export function isAsyncIterable(value: any): value is IAsyncIterable { return !isStrictNullOrUndefined(value) && isFunction(value[getKnownSymbol(WellKnownSymbols.asyncIterator)]); } \ No newline at end of file diff --git a/lib/src/iterator/types.ts b/lib/src/iterator/types.ts index 584e805f..eb07e86e 100644 --- a/lib/src/iterator/types.ts +++ b/lib/src/iterator/types.ts @@ -60,3 +60,46 @@ export type ArrToMapKeySelectorFn = (value: T, index: number) => string | num * ``` */ export type ArrToMapValueSelectorFn = (value: T, index: number) => V; + +/** + * The async iterator returned by an {@link IAsyncIterable}, it is compatible with the native `AsyncIterator` type + * which is only available when using the ES2018 (or later) lib. + * @since 0.20.0 + * @group Iterator + * @typeParam T - Identifies the type of the values returned by the async iterator + */ +export interface IAsyncIterator { + /** + * Returns a promise for the next result of the iteration. + * @param args - Optional value to pass to the iterator + */ + next(...args: any[]): Promise>; + + /** + * Optionally signals that the caller has finished with the iterator. + * @param value - The value to be returned + */ + return?(value?: any): Promise>; + + /** + * Optionally signals that the caller has detected an error. + * @param e - The error + */ + throw?(e?: any): Promise>; +} + +/** + * A value which can be iterated asynchronously, it is compatible with the native `AsyncIterable` type which is only + * available when using the ES2018 (or later) lib, so that the exported types can be used without that lib. + * - When the compiler can use `Symbol.asyncIterator` as a key (TypeScript v4.3 and later with the ES2018 or later lib) + * this contains the `[Symbol.asyncIterator]` function, so a native `AsyncIterable` can be assigned to it and vice-versa + * and it can be used with `for await`. + * - Earlier versions of TypeScript with the ES2018 or later lib define the symbol as a plain `symbol` which can't be + * used as a key, so this is `any` which can still be assigned to a native `AsyncIterable` and be used with `for await`. + * - Without the ES2018 or later lib `Symbol.asyncIterator` is not defined, so this is `object`. + * @since 0.20.0 + * @group Iterator + * @typeParam T - Identifies the type of the values returned by the async iterator + */ +export type IAsyncIterable = SymbolConstructor extends { readonly asyncIterator: infer S } ? + (symbol extends S ? any : { [K in Extract]: () => IAsyncIterator }) : object; diff --git a/lib/src/object/define.ts b/lib/src/object/define.ts index aa1fd681..79fc06c0 100644 --- a/lib/src/object/define.ts +++ b/lib/src/object/define.ts @@ -9,7 +9,7 @@ import { GET_OWN_PROPERTY_DESCRIPTOR, GET_OWN_PROPERTY_SYMBOLS, ObjClass } from "../internal/constants"; import { isFunction, isStrictUndefined } from "../helpers/base"; import { objForEachKey } from "./for_each_key"; -import { ILazyValue } from "../helpers/lazy"; +import { ILazyValue } from "../helpers/types"; import { _pureAssign, _pureRef } from "../internal/treeshake_helpers"; import { arrForEach } from "../array/forEach"; import { _objPropertyIsEnumerable } from "./property_is_enumerable"; @@ -88,9 +88,7 @@ export interface ObjDefinePropDescriptor { * @since 0.6.0 * @group Object */ -export type ObjDefinePropDescriptorMap = { - [key: PropertyKey]: ObjDefinePropDescriptor -}; +export type ObjDefinePropDescriptorMap = Record; /** * @internal diff --git a/lib/src/timer/idle.ts b/lib/src/timer/idle.ts index 3a2931ac..a7c99d44 100644 --- a/lib/src/timer/idle.ts +++ b/lib/src/timer/idle.ts @@ -15,6 +15,46 @@ import { scheduleTimeout } from "./timeout"; let _defaultIdleTimeout = 100; let _maxExecutionTime = 50; +/** + * The deadline passed to an idle callback, it describes how long the callback can run for. It has the same shape + * as the [IdleDeadline](https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline) provided by the DOM types + * of newer TypeScript versions, it is defined here so that the exported types can be used with earlier versions. + * @since 0.20.0 + * @group Timer + */ +export interface IIdleDeadline { + /** + * True if the callback is being called because the timeout specified when it was scheduled expired. + */ + readonly didTimeout: boolean; + + /** + * Returns the estimated number of milliseconds remaining in the current idle period. + */ + timeRemaining(): number; +} + +/** + * The signature of the callback passed to `requestIdleCallback` and {@link scheduleIdleCallback}. + * @since 0.20.0 + * @group Timer + * @param deadline - The deadline which describes how long the callback can run for. + */ +export type IdleCallbackFn = (deadline: IIdleDeadline) => void; + +/** + * The options for requesting an idle callback, it has the same shape as the `IdleRequestOptions` provided by the + * DOM types of newer TypeScript versions. + * @since 0.20.0 + * @group Timer + */ +export interface IIdleRequestOptions { + /** + * If the callback has not been called within this number of milliseconds it is scheduled to be called anyway. + */ + timeout?: number; +} + /** * Type alias for the global `requestIdleCallback` function, which schedules a callback to * be invoked during the browser's idle periods. Using a type alias makes it easier to @@ -65,7 +105,7 @@ let _maxExecutionTime = 50; * } * ``` */ -export type RequestIdleCallback = (callback: IdleRequestCallback, options?: IdleRequestOptions) => number; +export type RequestIdleCallback = (callback: IdleCallbackFn, options?: IIdleRequestOptions) => number; /** * Type alias for the global `cancelIdleCallback` function, which cancels a previously @@ -239,7 +279,7 @@ export function setDefaultMaxExecutionTime(maxTime: number) { * theIdleTimer.refresh(); * ``` */ -export function scheduleIdleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions): ITimerHandler { +export function scheduleIdleCallback(callback: IdleCallbackFn, options?: IIdleRequestOptions): ITimerHandler { function _createDeadline(timedOut: boolean) { let startTime = perfNow(); return { @@ -253,7 +293,7 @@ export function scheduleIdleCallback(callback: IdleRequestCallback, options?: Id if (hasIdleCallback()) { let handler = _createTimerHandler(true, (idleId: number) => { idleId && getCancelIdleCallback()(idleId); - return getIdleCallback()((deadline: IdleDeadline) => { + return getIdleCallback()((deadline: IIdleDeadline) => { handler.dn(); callback(deadline || _createDeadline(false)); }, options); diff --git a/lib/src/timer/interval.ts b/lib/src/timer/interval.ts index e3f8b7c7..f3192b20 100644 --- a/lib/src/timer/interval.ts +++ b/lib/src/timer/interval.ts @@ -77,6 +77,7 @@ export function scheduleInterval(callback: (...args: A) => void * ``` */ export function scheduleInterval(callback: (...args: A) => void, timeout: number): ITimerHandler { + // Copy the arguments as they are kept and reused by refresh() after this call has returned let theArguments = ArrSlice[CALL](arguments); let handler = _createTimerHandler(true, (intervalId: any) => { diff --git a/lib/src/timer/timeout.ts b/lib/src/timer/timeout.ts index e56822eb..1b36e18e 100644 --- a/lib/src/timer/timeout.ts +++ b/lib/src/timer/timeout.ts @@ -8,10 +8,13 @@ import { fnApply } from "../funcs/funcs"; import { isArray, isFunction } from "../helpers/base"; +import { ClearTimeoutOverrideFn, TimeoutOverrideFn, TimeoutOverrideFuncs } from "../helpers/types"; import { ArrSlice, CALL, UNDEF_VALUE } from "../internal/constants"; import { _getGlobalConfig } from "../internal/global"; import { ITimerHandler, _createTimerHandler } from "./handler"; +export { ClearTimeoutOverrideFn, TimeoutOverrideFn, TimeoutOverrideFuncs }; + // Package instance timeout override functions let _setTimeoutFn: TimeoutOverrideFn | undefined; let _clearTimeoutFn: ClearTimeoutOverrideFn | undefined; @@ -43,6 +46,13 @@ function _resolveClearTimeoutFn(timeoutFn: ClearTimeoutOverrideFn): ClearTimeout return result || clearTimeout; } +/** + * @internal + * @ignore + * Creates the timer handler, the callers MUST pass a copy of their arguments (not the arguments object itself) + * as the callback (`theArgs[0]`) is replaced with a wrapper and the arguments are kept and reused by `refresh()` + * after the callers have returned. + */ function _createTimeoutWith(startTimer: boolean, overrideFn: TimeoutOverrideFn | TimeoutOverrideFuncs, theArgs: any[]): ITimerHandler { let isArr = isArray(overrideFn); let len = isArr ? overrideFn.length : 0; @@ -65,7 +75,9 @@ function _createTimeoutWith(startTimer: boolean, overrideFn: TimeoutOverrideFn | } handler.dn(); - fnApply(timerFn, UNDEF_VALUE, ArrSlice[CALL](arguments)); + // Pass this callbacks arguments directly (apply accepts any array-like), this function has no named + // parameters and the arguments are not modified, so there is no need to slice them into a new array + fnApply(timerFn, UNDEF_VALUE, arguments); }; let handler = _createTimerHandler(startTimer, (timerId?: any) => { @@ -233,37 +245,6 @@ export function setGlobalTimeoutOverrides(overrideFn?: TimeoutOverrideFn | Timeo } } -/** - * The signature of the override timeout function used to create a new timeout instance, it follows the same signature as - * the native `setTimeout` API. - * @since 0.4.4 - * @group Timer - * @param callback - A function to be executed after the timer expires. - * @param ms - The time, in milliseconds that the timer should wait before the specified function or code is executed. - * If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, the next event cycle. - * @param args - Additional arguments which are passed through to the function specified by callback. - * @return The returned timeoutID is a positive integer value which identifies the timer created by the call to setTimeout(). - * This value can be passed to clearTimeout() to cancel the timeout. - */ -export type TimeoutOverrideFn = (callback: (...args: TArgs) => void, ms?: number, ...args: TArgs) => number | any; - -/** - * The signatire of the function to override clearing a previous timeout created with the {@link TimeoutOverrideFn}, it will be passed - * the result returned from the {@link TimeoutOverrideFn} call. - * @since 0.4.5 - * @group Timer - * @param timeoutId - The value returned from the previous {@link TimeoutOverrideFn}. - */ -export type ClearTimeoutOverrideFn = (timeoutId: number | any) => void; - -/** - * Defines the array signature used to pass the override set and clean functions for creating a timeout. - * The first index [0] is the set function to be used and the second index [1] is the clear function to be used. - * @since 0.4.5 - * @group Timer - */ -export type TimeoutOverrideFuncs = [ TimeoutOverrideFn | null, ClearTimeoutOverrideFn | null ]; - /** * Creates and starts a timer which executes a function or specified piece of code once the timer expires, this is simular * to using `setTimeout` but provides a return object for cancelling and restarting (refresh) the timer. diff --git a/lib/test/src/common/funcs/proxy.test.ts b/lib/test/src/common/funcs/proxy.test.ts index 46ccd8a2..4c1807ca 100644 --- a/lib/test/src/common/funcs/proxy.test.ts +++ b/lib/test/src/common/funcs/proxy.test.ts @@ -50,6 +50,33 @@ describe("function proxy helpers", () => { assert.equal(module2.defX(), 42); assert.equal(module2.log("Darkness"), "Hello Darkness : 42"); }); + + it("passes all of the arguments and resolves the host on every call", () => { + interface IHost { + name: string; + getArgs(...args: any[]): any[]; + } + + let getArgs = function(this: IHost, ..._args: any[]) { + return [ this.name, arguments.length ].concat(Array.prototype.slice.call(arguments)); + }; + + let host1: IHost = { name: "host1", getArgs: getArgs }; + let host2: IHost = { name: "host2", getArgs: getArgs }; + let theHost = host1; + let hostCalls = 0; + let proxyFn = createFnDeferredProxy any[]>(() => { + hostCalls++; + return theHost; + }, "getArgs"); + + assert.deepEqual(proxyFn(), [ "host1", 0 ]); + assert.deepEqual(proxyFn(1, "two", undefined), [ "host1", 3, 1, "two", undefined ]); + + theHost = host2; + assert.deepEqual(proxyFn(null), [ "host2", 1, null ]); + assert.equal(hostCalls, 3, "The host is resolved for every call"); + }); }); describe("createProxyFuncs", () => { diff --git a/lib/test/src/common/helpers/array.test.ts b/lib/test/src/common/helpers/array.test.ts index f165f7f1..77ad3095 100644 --- a/lib/test/src/common/helpers/array.test.ts +++ b/lib/test/src/common/helpers/array.test.ts @@ -1024,6 +1024,61 @@ describe("array helpers", () => { assert.deepEqual(arrSlice(lyrics, -2), [ "to", "talk" ]); assert.deepEqual(arrSlice(lyrics, 2, -1), [ "my", "old", "friend.", "I've", "come", "to" ]); }); + + it("array-like values and the start / end arguments", () => { + let arrayLike: ArrayLike = { length: 3, 0: "a", 1: "b", 2: "c" }; + assert.deepEqual(arrSlice(arrayLike), [ "a", "b", "c" ]); + assert.deepEqual(arrSlice(arrayLike, 1), [ "b", "c" ]); + assert.deepEqual(arrSlice(arrayLike, undefined, 2), [ "a", "b" ]); + assert.deepEqual(arrSlice(arrayLike, -2, -1), [ "b" ]); + assert.deepEqual(arrSlice({ length: 0 }), []); + + (function(..._args: any[]) { + assert.deepEqual(arrSlice(arguments), [ 1, "two", undefined ]); + assert.deepEqual(arrSlice(arguments, 1), [ "two", undefined ]); + })(1, "two", undefined); + }); + + it("preserves empty slots", () => { + let sparse: number[] = [ 1 ]; + sparse[2] = 3; + + let result = arrSlice(sparse); + assert.equal(result.length, 3); + assert.ok(!(1 in result), "the empty slot is preserved"); + assert.equal(result[2], 3); + }); + + it("uses the value's own slice function", () => { + let custom: any = { + length: 2, + 0: 1, + 1: 2, + slice: function(this: any, start?: number, end?: number) { + return [ this === custom, arguments.length, start, end ]; + } + }; + + assert.deepEqual(arrSlice(custom), [ true, 0, undefined, undefined ]); + assert.deepEqual(arrSlice(custom, 1, 2), [ true, 2, 1, 2 ]); + + // Strings use String.prototype.slice + assert.equal(arrSlice("hello" as any, 1, 3) as any, "el"); + }); + + it("throws a TypeError for null and undefined", () => { + let invalid: any[] = [ null, undefined ]; + for (let lp = 0; lp < invalid.length; lp++) { + let caught: any = null; + try { + arrSlice(invalid[lp]); + } catch (e) { + caught = e; + } + + assert.ok(caught instanceof TypeError, "Expected a TypeError for " + String(invalid[lp]) + " - " + String(caught)); + } + }); }); describe("arrUnique", () => { diff --git a/lib/test/src/common/helpers/throw.test.ts b/lib/test/src/common/helpers/throw.test.ts index c0754dcf..5c88a6f2 100644 --- a/lib/test/src/common/helpers/throw.test.ts +++ b/lib/test/src/common/helpers/throw.test.ts @@ -382,6 +382,23 @@ describe("throw helpers", () => { assert.ok(isError(err), "isError returns true"); assert.equal(err.message, "hello", "Message is set correctly"); }); + + it("passes all of the arguments to the base class when there is no superArgsFn", () => { + let baseArgs: any[] = null; + let BaseErrorCls = createCustomError("BaseArgsError", (_self, args) => { + baseArgs = Array.prototype.slice.call(args); + }); + let ChildErrorCls = createCustomError("ChildArgsError", null, BaseErrorCls); + + let err = new ChildErrorCls("hello", 1, undefined, null); + assert.deepEqual(baseArgs, [ "hello", 1, undefined, null ], "The base class received all of the arguments"); + assert.equal(err.message, "hello", "Message is set correctly"); + assert.ok(err instanceof BaseErrorCls, "is a BaseArgsError"); + + baseArgs = null; + new ChildErrorCls(); + assert.deepEqual(baseArgs, [], "The base class received no arguments"); + }); }); describe("createCustomError internal stack capture optimization", () => { diff --git a/lib/test/src/common/helpers/types.test.ts b/lib/test/src/common/helpers/types.test.ts index 9bf95fa4..666f2037 100644 --- a/lib/test/src/common/helpers/types.test.ts +++ b/lib/test/src/common/helpers/types.test.ts @@ -7,7 +7,10 @@ */ import { assert } from "@nevware21/tripwire-chai"; -import { DeepPartial, DeepReadonly, DeepRequired, Mutable, NonEmptyArray, ReadonlyRecord, ValueOf } from "../../../../src/index"; +import { + DeepPartial, DeepReadonly, DeepRequired, IdleCallbackFn, IIdleDeadline, IIdleRequestOptions, Mutable, + NonEmptyArray, ObjDefinePropDescriptorMap, objDefineProps, ReadonlyRecord, RequestIdleCallback, ValueOf +} from "../../../../src/index"; type IsEqual = (() => V extends T ? 1 : 2) extends @@ -64,4 +67,69 @@ describe("typing helper aliases", () => { assert.equal(allGood, true, "Type alias compile checks should pass"); }); + + it("should map arrays to readonly arrays of the deep type", () => { + type _partialArray = AssertTrue, ReadonlyArray>>; + type _partialReadonlyArray = AssertTrue>, ReadonlyArray>>; + type _partialNestedArray = AssertTrue, ReadonlyArray>>>; + type _readonlyArray = AssertTrue, ReadonlyArray>>; + type _readonlyNestedArray = AssertTrue, ReadonlyArray>>>; + type _requiredArray = AssertTrue>, ReadonlyArray>>; + type _partialUnion = AssertTrue, ReadonlyArray | number>>; + type _partialFunction = AssertTrue void>, () => void>>; + type _readonlyTuple = AssertTrue, ReadonlyArray>>; + type _partialTuple = AssertTrue, ReadonlyArray>>; + + type ArrayOfObjects = DeepPartial<{ items: Array<{ id: number; tag: string }> }>; + type _arrayOfObjects = AssertTrue | undefined>>; + + type Node = { next: Node[]; value: string }; + const recursive: DeepPartial = { next: [ { next: [ { value: "c" } ] } ], value: "a" }; + const required: DeepRequired = { next: [], value: "b" }; + + assert.equal(recursive.value, "a", "Recursive types can be used"); + assert.equal(required.next.length, 0, "Recursive types can be used"); + }); + + it("should support string, number and symbol keys for ObjDefinePropDescriptorMap", () => { + const theSymbol = Symbol("key"); + const props: ObjDefinePropDescriptorMap = { + name: { v: "a" }, + 1: { v: "b" }, + [theSymbol]: { v: "c" } + }; + + const target: any = objDefineProps({}, props); + + assert.equal(target.name, "a", "String key"); + assert.equal(target[1], "b", "Number key"); + assert.equal(target[theSymbol], "c", "Symbol key"); + }); + + it("should be compatible with the native idle callback types", () => { + let deadlines = 0; + const nativeCallback: IdleRequestCallback = (deadline: IdleDeadline) => { + deadlines += deadline.timeRemaining(); + }; + + // Each of the types must be assignable in both directions + const callback: IdleCallbackFn = nativeCallback; + const nativeFromCallback: IdleRequestCallback = callback; + const deadline: IIdleDeadline = { didTimeout: false, timeRemaining: () => 1 }; + const nativeDeadline: IdleDeadline = deadline; + const deadlineFromNative: IIdleDeadline = nativeDeadline; + const nativeOptions: IdleRequestOptions = { timeout: 10 }; + const options: IIdleRequestOptions = nativeOptions; + const optionsToNative: IdleRequestOptions = options; + + // The native function can be used for the override type and vice-versa + const nativeRequest: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number = () => 42; + const request: RequestIdleCallback = nativeRequest; + const requestToNative: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number = request; + + nativeFromCallback(deadlineFromNative); + assert.equal(deadlines, 1, "The callback was called with the deadline"); + assert.equal(optionsToNative.timeout, 10, "The options are retained"); + assert.equal(requestToNative(callback), 42, "The native function was called"); + }); }); diff --git a/lib/test/src/esnext/helpers/base.test.ts b/lib/test/src/esnext/helpers/base.test.ts index 651d7e4a..e185cf42 100644 --- a/lib/test/src/esnext/helpers/base.test.ts +++ b/lib/test/src/esnext/helpers/base.test.ts @@ -8,6 +8,7 @@ import { assert } from "@nevware21/tripwire-chai"; import { isAsyncFunction, isGenerator, isAsyncGenerator, isFunction } from "../../../../src/helpers/base"; +import { IAsyncGeneratorFunction } from "../../../../src/helpers/types"; // This test suite is designed to check the functionality of the base helper functions // As TypeScript rewrites the async and generator functions when the target is ES5, and @@ -77,6 +78,24 @@ describe("ESNext base tests", () => { assert.equal(isAsyncGenerator("string"), false, "Should handle string"); assert.equal(isAsyncGenerator(true), false, "Should handle boolean"); }); + + it("should narrow so the value can be used as a native AsyncGeneratorFunction", () => { + function narrowAny(value: any): AsyncGeneratorFunction | null { + return isAsyncGenerator(value) ? value : null; + } + + function narrowKnown(value: AsyncGeneratorFunction | string): AsyncGeneratorFunction | null { + return isAsyncGenerator(value) ? value : null; + } + + const guard: (value: any) => value is IAsyncGeneratorFunction = isAsyncGenerator; + + assert.equal(guard(_asyncGeneratorFunction), true, "The guard identifies an async generator function"); + assert.equal(narrowAny(_asyncGeneratorFunction), _asyncGeneratorFunction as any, "An async generator function is narrowed"); + assert.equal(narrowAny(_regularFunction), null, "A regular function is not narrowed"); + assert.equal(narrowKnown(_asyncGeneratorFunction as any), _asyncGeneratorFunction as any, "A native type is retained"); + assert.equal(narrowKnown("a"), null, "A string is not narrowed"); + }); }); describe("isFunction", () => { diff --git a/lib/test/src/esnext/iterator/iterator.test.ts b/lib/test/src/esnext/iterator/iterator.test.ts index ea4e9a67..bbde55f7 100644 --- a/lib/test/src/esnext/iterator/iterator.test.ts +++ b/lib/test/src/esnext/iterator/iterator.test.ts @@ -8,6 +8,7 @@ import { assert } from "@nevware21/tripwire-chai"; import { isAsyncIterable, isIterable, isIterator } from "../../../../src/iterator/iterator"; +import { IAsyncIterable } from "../../../../src/iterator/types"; describe("ESNext iterator tests", () => { async function* _asyncGenerator() { @@ -40,6 +41,47 @@ describe("ESNext iterator tests", () => { assert.equal(isAsyncIterable({ [Symbol.asyncIterator]: 1 } as any), false, "Should require function async iterator"); }); + it("should narrow a value so it can be used as a native AsyncIterable", async () => { + async function _sum(value: any): Promise { + let total = 0; + if (isAsyncIterable(value)) { + for await (const item of value) { + total += item; + } + } + + return total; + } + + function _toNative(value: unknown): AsyncIterable | null { + return isAsyncIterable(value) ? value : null; + } + + assert.equal(await _sum(_asyncGenerator()), 1, "Should iterate the narrowed value"); + assert.equal(await _sum("value"), 0, "Should not iterate values that are not async iterable"); + assert.ok(_toNative(_asyncGenerator()), "Should return the async iterable"); + assert.equal(_toNative(1), null, "Should not return other values"); + }); + + it("should accept native AsyncIterable values and retain their type", async () => { + const native: AsyncIterable = _asyncGenerator(); + const mixed = native as AsyncIterable | string; + const compatible: IAsyncIterable = native; + const backToNative: AsyncIterable = compatible; + + let total = 0; + if (isAsyncIterable(native)) { + for await (const item of native) { + const value: number = item; + total += value; + } + } + + assert.equal(total, 1, "A native AsyncIterable is identified and keeps its type"); + assert.equal(isAsyncIterable(mixed) ? "async" : mixed.charAt(0), "async", "A union is narrowed to the native type"); + assert.equal(isAsyncIterable(backToNative), true, "The compatible type can be assigned back to the native type"); + }); + it("should remain distinct from sync iterator checks", () => { let asyncGenerator = _asyncGenerator();