UNPKG

ts-data-forge

Version:

[![npm version](https://img.shields.io/npm/v/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge) [![npm downloads](https://img.shields.io/npm/dm/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge) [![License](https://img.shields.

1,221 lines (1,219 loc) 73.6 kB
import '../collections/imap-mapped.mjs'; import { IMap } from '../collections/imap.mjs'; import '../collections/iset-mapped.mjs'; import '../collections/iset.mjs'; import { isString, isUndefined } from '../guard/is-type.mjs'; import { Optional } from '../functional/optional.mjs'; import { pipe } from '../functional/pipe.mjs'; import { Result } from '../functional/result.mjs'; import { range } from '../iterator/range.mjs'; import '../number/branded-types/finite-number.mjs'; import '../number/branded-types/int.mjs'; import '../number/branded-types/int16.mjs'; import '../number/branded-types/int32.mjs'; import '../number/branded-types/non-negative-finite-number.mjs'; import '../number/branded-types/non-negative-int16.mjs'; import '../number/branded-types/non-negative-int32.mjs'; import '../number/branded-types/non-zero-finite-number.mjs'; import '../number/branded-types/non-zero-int.mjs'; import '../number/branded-types/non-zero-int16.mjs'; import '../number/branded-types/non-zero-int32.mjs'; import '../number/branded-types/non-zero-safe-int.mjs'; import '../number/branded-types/non-zero-uint16.mjs'; import '../number/branded-types/non-zero-uint32.mjs'; import '../number/branded-types/positive-finite-number.mjs'; import '../number/branded-types/positive-int.mjs'; import '../number/branded-types/positive-int16.mjs'; import '../number/branded-types/positive-int32.mjs'; import '../number/branded-types/positive-safe-int.mjs'; import '../number/branded-types/positive-uint16.mjs'; import { asPositiveUint32 } from '../number/branded-types/positive-uint32.mjs'; import '../number/branded-types/safe-int.mjs'; import '../number/branded-types/safe-uint.mjs'; import '../number/branded-types/uint.mjs'; import '../number/branded-types/uint16.mjs'; import { Uint32, asUint32 } from '../number/branded-types/uint32.mjs'; import '../number/enum/int8.mjs'; import '../number/enum/uint8.mjs'; import { Num } from '../number/num.mjs'; import '../number/refined-number-utils.mjs'; import { castMutable } from '../others/cast-mutable.mjs'; import { tp } from '../others/tuple.mjs'; import { unknownToString } from '../others/unknown-to-string.mjs'; /** * A comprehensive, immutable utility library for array manipulations in TypeScript. * Provides a wide range of functions for array creation, validation, transformation, * reduction, slicing, set operations, and more, with a focus on type safety and * leveraging TypeScript's type inference capabilities. * All functions operate on `readonly` arrays and return new `readonly` arrays, * ensuring immutability. */ var Arr; (function (Arr) { /** * Returns the size (length) of an array as a type-safe branded integer. * * This function provides the array length with enhanced type safety through branded types: * - For arrays known to be non-empty at compile time: returns `PositiveNumber & SizeType.Arr` * - For general arrays that may be empty: returns `SizeType.Arr` (branded Uint32) * * The returned value is always a non-negative integer that can be safely used for array indexing * and size comparisons. The branded type prevents common integer overflow issues and provides * better type checking than plain numbers. * * @template Ar The exact type of the input array, used for precise return type inference. * @param array The array to measure. Can be any readonly array type. * @returns The length of the array as a branded type: * - `IntersectBrand<PositiveNumber, SizeType.Arr>` for known non-empty arrays * - `SizeType.Arr` for general arrays (branded Uint32, may be 0) * * @example * ```typescript * // Known non-empty arrays get positive branded type * const tuple = [1, 2, 3] as const; * const tupleSize = Arr.size(tuple); * // Type: IntersectBrand<PositiveNumber, SizeType.Arr> * // Value: 3 (branded, guaranteed positive) * * const nonEmpty: NonEmptyArray<string> = ['a', 'b']; * const nonEmptySize = Arr.size(nonEmpty); * // Type: IntersectBrand<PositiveNumber, SizeType.Arr> * // Guaranteed to be > 0 * * // General arrays may be empty, get regular branded type * const generalArray: number[] = [1, 2, 3]; * const generalSize = Arr.size(generalArray); * // Type: SizeType.Arr (branded Uint32) * // May be 0 or positive * * // Empty arrays * const emptyArray = [] as const; * const emptySize = Arr.size(emptyArray); * // Type: SizeType.Arr * // Value: 0 (branded) * * // Runtime arrays with unknown content * const dynamicArray = Array.from({ length: Math.random() * 10 }, (_, i) => i); * const dynamicSize = Arr.size(dynamicArray); * // Type: SizeType.Arr (may be 0) * * // Using size for safe operations * const data = [10, 20, 30]; * const dataSize = Arr.size(data); * * // Safe for array creation * const indices = Arr.seq(dataSize); // Creates [0, 1, 2] * const zeros = Arr.zeros(dataSize); // Creates [0, 0, 0] * * // Functional composition * const arrays = [ * [1, 2], * [3, 4, 5], * [], * [6] * ]; * const sizes = arrays.map(Arr.size); // [2, 3, 0, 1] (all branded) * const totalElements = sizes.reduce(Uint32.add, 0); // 6 * * // Type guards work with size * if (Arr.size(data) > 0) { * // TypeScript knows data is non-empty here * const firstElement = data[0]; // Safe access * } * * // Type inference examples * expectType<typeof tupleSize, IntersectBrand<PositiveNumber, SizeType.Arr>>('='); * expectType<typeof generalSize, SizeType.Arr>('='); * expectType<typeof emptySize, SizeType.Arr>('='); * ``` * * @see {@link length} - Alias for this function * @see {@link isEmpty} for checking if size is 0 * @see {@link isNonEmpty} for checking if size > 0 */ Arr.size = (array) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion array.length; // type guard /** * Type guard that checks if a value is an array, excluding types that cannot be arrays. * This function refines the type by filtering out non-array types from unions. * @template E The input type that may or may not be an array. * @param value The value to check. * @returns `true` if the value is an array, `false` otherwise. * @example * ```ts * function processValue(value: string | number[] | null) { * if (Arr.isArray(value)) { * // value is now typed as number[] * console.log(value.length); * } * } * * Arr.isArray([1, 2, 3]); // true * Arr.isArray("hello"); // false * Arr.isArray(null); // false * ``` */ Arr.isArray = (value) => Array.isArray(value); // validation /** * Type guard that checks if an array is empty (has no elements). * * This function serves as both a runtime check and a TypeScript type guard, * narrowing the array type to `readonly []` when the check passes. It's useful * for conditional logic and type-safe handling of potentially empty arrays. * * @template E The type of elements in the array. * @param array The array to check for emptiness. * @returns `true` if the array has length 0, `false` otherwise. * When `true`, TypeScript narrows the type to `readonly []`. * * @example * ```typescript * // Basic emptiness checking * const emptyArray: number[] = []; * const nonEmptyArray = [1, 2, 3]; * * console.log(Arr.isEmpty(emptyArray)); // true * console.log(Arr.isEmpty(nonEmptyArray)); // false * * // Type guard behavior * function processArray(arr: readonly number[]) { * if (Arr.isEmpty(arr)) { * // arr is now typed as readonly [] * console.log('Array is empty'); * // arr[0]; // type error! * return 0; * } * } * * // Early returns * function sumArray(numbers: readonly number[]): number { * if (Arr.isEmpty(numbers)) { * return 0; // Handle empty case early * } * return numbers.reduce((sum, n) => sum + n, 0); * } * * ``` * * @see {@link isNonEmpty} for the opposite check (non-empty arrays) * @see {@link size} for getting the exact length * @see {@link isArrayOfLength} for checking specific lengths */ Arr.isEmpty = (array) => array.length === 0; /** * Type guard that checks if an array is non-empty (has at least one element). * * This function serves as both a runtime check and a TypeScript type guard, * narrowing the array type to `NonEmptyArray<E>` when the check passes. This enables * safe access to array elements without undefined checks, as TypeScript knows the array * has at least one element. * * @template E The type of elements in the array. * @param array The array to check for non-emptiness. * @returns `true` if the array has length > 0, `false` otherwise. * When `true`, TypeScript narrows the type to `NonEmptyArray<E>`. * * @example * ```typescript * // Basic non-emptiness checking * const emptyArray: number[] = []; * const nonEmptyArray = [1, 2, 3]; * * console.log(Arr.isNonEmpty(emptyArray)); // false * console.log(Arr.isNonEmpty(nonEmptyArray)); // true * * // Type guard behavior enables safe element access * function getFirstElement(arr: readonly number[]): number | undefined { * if (Arr.isNonEmpty(arr)) { * // arr is now typed as NonEmptyArray<number> * return arr[0]; // Safe - no undefined, TypeScript knows this exists * } * return undefined; * } * * // Safe operations on non-empty arrays * function processData(data: readonly string[]) { * if (!Arr.isNonEmpty(data)) return; // early return pattern * * // This is now safe without undefined checks * const first = data[0]; * * // Can safely use non-empty array methods * const lastElement = Arr.last(data); * * } * * // Filtering and working with arrays * const possiblyEmptyArrays: readonly number[][] = [ * [1, 2, 3], * [], * [4, 5], * [] * ]; * * // Get only non-empty arrays with proper typing * const definitelyNonEmpty = possiblyEmptyArrays.filter(Arr.isNonEmpty); * // Type: NonEmptyArray<number>[] * * // Now safe to access elements * const firstElements = definitelyNonEmpty.map(arr => arr[0]); // [1, 4] * * // Early validation * function calculateAverage(numbers: readonly number[]): number { * if (!Arr.isNonEmpty(numbers)) { * throw new Error('Cannot calculate average of empty array'); * } * * // numbers is now NonEmptyArray<number> * return Arr.sum(numbers) / Arr.size(numbers); * } * * // Functional composition * const arrayGroups = [ * [1, 2], * [], * [3, 4, 5], * [] * ]; * * const nonEmptyGroups = arrayGroups * .filter(Arr.isNonEmpty) // Filter to NonEmptyArray<number>[] * .map(group => group[0]); // Safe access to first element: [1, 3] * * // Combined with other array operations * function processArraySafely<T>( * arr: readonly T[], * processor: (item: T) => string * ): string { * if (Arr.isNonEmpty(arr)) { * return arr.map(processor).join(' -> '); * } * return 'No items to process'; * } * * // Type inference examples * const testArray = [1, 2, 3]; * const isNonEmptyResult = Arr.isNonEmpty(testArray); * * expectType<typeof isNonEmptyResult, boolean>('='); * expectType<Parameters<typeof Arr.isNonEmpty>[0], readonly unknown[]>('='); * * // Type narrowing in conditional * if (Arr.isNonEmpty(testArray)) { * expectType<typeof testArray, NonEmptyArray<number>>('='); * } * ``` * * @see {@link isEmpty} for the opposite check (empty arrays) * @see {@link size} for getting the exact length * @see {@link head} for safely getting the first element * @see {@link last} for safely getting the last element */ Arr.isNonEmpty = (array) => array.length > 0; /** * Checks if an array has a specific length. * @template E The type of elements in the array. * @template N The expected length of the array (must be a number type). * @param array The array to check. * @param len The expected length. * @returns `true` if the array has the specified length, `false` otherwise. * @example * ```ts * const arr: readonly number[] = [1, 2, 3]; * if (Arr.isArrayOfLength(arr, 3)) { * // arr is now typed as readonly [number, number, number] * } * Arr.isArrayOfLength([1, 2], 3); // false * ``` */ Arr.isArrayOfLength = (array, len) => array.length === len; /** * Checks if an array has at least a specific length. * @template E The type of elements in the array. * @template N The minimum expected length of the array (must be a number type). * @param array The array to check. * @param len The minimum expected length. * @returns `true` if the array has at least the specified length, `false` otherwise. * @example * ```ts * const arr: readonly number[] = [1, 2, 3]; * if (Arr.isArrayAtLeastLength(arr, 2)) { * // arr is now typed as readonly [number, number, ...number[]] * } * Arr.isArrayAtLeastLength([1], 2); // false * ``` */ Arr.isArrayAtLeastLength = (array, len) => array.length >= len; /** * Checks if an index is within the valid range of an array (i.e., `0 <= index < array.length`). * @template E The type of elements in the array. * @param array The input array. * @param index The index to check. * @returns `true` if the index is within the array bounds, `false` otherwise. * @example * ```ts * Arr.indexIsInRange([10, 20], 0); // true * Arr.indexIsInRange([10, 20], 1); // true * Arr.indexIsInRange([10, 20], 2); // false * Arr.indexIsInRange([10, 20], -1); // false * Arr.indexIsInRange([], 0); // false * ``` */ Arr.indexIsInRange = (array, index) => Num.isInRange(0, array.length)(index); // array creation /** * Creates an array of zeros with the specified length. * * This function provides compile-time type safety with precise return types: * - When `len` is a compile-time known `SmallUint` (0-100), returns a tuple with exact length * - When `len` is a positive runtime value, returns a `NonEmptyArray<0>` * - Otherwise, returns a `readonly 0[]` that may be empty * * @template N The type of the length parameter. When a `SmallUint` literal is provided, * the return type will be a tuple of exactly that length filled with zeros. * @param len The length of the array to create. Must be a non-negative integer. * @returns An immutable array of zeros. The exact return type depends on the input: * - `ArrayOfLength<N, 0>` when `N` is a `SmallUint` literal * - `NonEmptyArray<0>` when `len` is a positive runtime value * - `readonly 0[]` for general non-negative values * * @example * ```typescript * // Compile-time known lengths produce precise tuple types * const exactLength = Arr.zeros(3); // readonly [0, 0, 0] * const empty = Arr.zeros(0); // readonly [] * * // Runtime positive values produce non-empty arrays * const count = Math.floor(Math.random() * 5) + 1; * const nonEmpty = Arr.zeros(count); // NonEmptyArray<0> * * // General runtime values may be empty * const maybeEmpty = Arr.zeros(Math.floor(Math.random() * 5)); // readonly 0[] * * // Type inference examples * expectType<typeof exactLength, readonly [0, 0, 0]>('='); * expectType<typeof empty, readonly []>('='); * expectType<typeof nonEmpty, NonEmptyArray<0>>('='); * expectType<typeof maybeEmpty, readonly 0[]>('='); * ``` */ Arr.zeros = (len) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion Array.from({ length: len }).fill(0); /** * Creates a sequence of consecutive integers from 0 to `len-1`. * * This function generates index sequences with precise compile-time typing: * - When `len` is a compile-time known `SmallUint` (0-100), returns a tuple of consecutive integers * - When `len` is a positive runtime value, returns a `NonEmptyArray<SizeType.Arr>` * - Otherwise, returns a `readonly SizeType.Arr[]` that may be empty * * @template N The type of the length parameter. When a `SmallUint` literal is provided, * the return type will be a tuple containing the sequence [0, 1, 2, ..., N-1]. * @param len The length of the sequence to create. Must be a non-negative integer. * @returns An immutable array containing the sequence [0, 1, 2, ..., len-1]. * The exact return type depends on the input: * - `Seq<N>` (precise tuple) when `N` is a `SmallUint` literal * - `NonEmptyArray<SizeType.Arr>` when `len` is a positive runtime value * - `readonly SizeType.Arr[]` for general non-negative values * * @example * ```typescript * // Compile-time known lengths produce precise tuple types * const indices = Arr.seq(4); // readonly [0, 1, 2, 3] * const empty = Arr.seq(0); // readonly [] * const single = Arr.seq(1); // readonly [0] * * // Runtime positive values produce non-empty arrays * const count = Math.floor(Math.random() * 5) + 1; * const nonEmpty = Arr.seq(count); // NonEmptyArray<SizeType.Arr> * * // General runtime values may be empty * const maybeEmpty = Arr.seq(Math.floor(Math.random() * 5)); // readonly SizeType.Arr[] * * // Useful for generating array indices * const data = ['a', 'b', 'c', 'd']; * const indexSequence = Arr.seq(data.length); // [0, 1, 2, 3] * * // Type inference examples * expectType<typeof indices, readonly [0, 1, 2, 3]>('='); * expectType<typeof empty, readonly []>('='); * expectType<typeof single, readonly [0]>('='); * ``` */ Arr.seq = (len) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion Array.from({ length: len }, (_, i) => i); /** * Creates a new array of the specified length, with each position filled with the provided initial value. * * This function provides compile-time type safety with precise return types and performs shallow copying * of the initial value (the same reference is used for all positions): * - When `len` is a compile-time known `SmallUint` (0-100), returns a tuple of exactly that length * - When `len` is a positive runtime value, returns a `NonEmptyArray<V>` * - Otherwise, returns a `readonly V[]` that may be empty * * @template V The type of the initial value. The `const` constraint preserves literal types. * @template N The type of the length parameter when it's a `SmallUint` literal. * @param len The length of the array to create. Must be a non-negative integer. * @param init The value to fill each position with. The same reference is used for all positions. * @returns An immutable array filled with the initial value. The exact return type depends on the length: * - `ArrayOfLength<N, V>` when `len` is a `SmallUint` literal * - `NonEmptyArray<V>` when `len` is a positive runtime value * - `readonly V[]` for general non-negative values * * @example * ```typescript * // Compile-time known lengths produce precise tuple types * const strings = Arr.create(3, 'hello'); // readonly ['hello', 'hello', 'hello'] * const numbers = Arr.create(2, 42); // readonly [42, 42] * const empty = Arr.create(0, 'unused'); // readonly [] * * // Object references are shared (shallow copy behavior) * const obj = { id: 1, name: 'test' }; * const objects = Arr.create(3, obj); // readonly [obj, obj, obj] * objects[0] === objects[1]; // true - same reference * objects[0].id = 999; // Mutates the shared object * console.log(objects[1].id); // 999 - all positions affected * * // Runtime positive values produce non-empty arrays * const count = Math.floor(Math.random() * 5) + 1; * const nonEmpty = Arr.create(count, 'item'); // NonEmptyArray<string> * * // Literal type preservation with const assertion * const literals = Arr.create(2, 'success' as const); // readonly ['success', 'success'] * * // Type inference examples * expectType<typeof strings, readonly ['hello', 'hello', 'hello']>('='); * expectType<typeof numbers, readonly [42, 42]>('='); * expectType<typeof empty, readonly []>('='); * ``` * * @see {@link zeros} for creating arrays filled with zeros * @see {@link seq} for creating sequences of consecutive integers */ Arr.create = (len, init) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion Array.from({ length: Math.max(0, len) }, () => init); /** * Creates an array from a generator function. * * This utility function provides enhanced type safety by constraining the generator function * to prevent incorrect return values. The generator can only yield values of type T and * must return void, which helps catch common mistakes like returning values instead of yielding. * * @template T - The type of elements in the generated array * @param generatorFn - A function that returns a generator yielding elements of type T * @returns A readonly array containing all yielded values from the generator * * @example * ```typescript * const nums:readonly number[] = Arr.generate<number>(function* () { * yield 1; * yield* [2, 3]; * }); * * assert.deepStrictEqual(nums, [1, 2, 3]); * * // Type safety - prevents incorrect returns: * const nums2 = Arr.generate<number>(function* () { * yield 1; * if (someCondition) { * return; // OK - returning is allowed, but must be void * } * yield* [2, 3]; * // return 1; // NG - TypeScript error, cannot return T * }); * ``` */ Arr.generate = (generatorFn) => Array.from(generatorFn()); /** * Creates a shallow copy of an array, preserving the exact type signature. * * This function creates a new array with the same elements as the input, but with a new array reference. * Object references within the array are preserved (shallow copy), and the readonly/mutable status * of the array type is maintained. * * @template Ar The exact type of the input array, preserving tuple types, readonly status, and element types. * @param array The array to copy. Can be any array type: mutable, readonly, tuple, or general array. * @returns A new array that is a shallow copy of the input. The return type exactly matches the input type, * preserving readonly status, tuple structure, and element types. * * @example * ```typescript * // Mutable arrays remain mutable * const mutableOriginal = [1, 2, 3]; * const mutableCopy = Arr.copy(mutableOriginal); // number[] * mutableCopy[0] = 999; // OK - still mutable * mutableOriginal[0]; // 1 - original unchanged * * // Readonly arrays remain readonly * const readonlyOriginal = [1, 2, 3] as const; * const readonlyCopy = Arr.copy(readonlyOriginal); // readonly [1, 2, 3] * // readonlyCopy[0] = 999; // Error - readonly array * * // Tuple types are preserved * const tupleOriginal: [string, number, boolean] = ['hello', 42, true]; * const tupleCopy = Arr.copy(tupleOriginal); // [string, number, boolean] * * // Shallow copy behavior with objects * const objectArray = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; * const objectCopy = Arr.copy(objectArray); * objectCopy[0].name = 'Charlie'; // Mutates the shared object reference * console.log(objectArray[0].name); // 'Charlie' - original affected * objectCopy.push({ id: 3, name: 'Dave' }); // Array structure changes don't affect original * console.log(objectArray.length); // 2 - original array length unchanged * * // Empty arrays * const emptyArray: number[] = []; * const emptyCopy = Arr.copy(emptyArray); // number[] * const emptyTuple = [] as const; * const emptyTupleCopy = Arr.copy(emptyTuple); // readonly [] * * // Type inference examples * expectType<typeof mutableCopy, number[]>('='); * expectType<typeof readonlyCopy, readonly [1, 2, 3]>('='); * expectType<typeof tupleCopy, [string, number, boolean]>('='); * ``` * * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice | Array.prototype.slice} * The underlying implementation uses `slice()` for efficient shallow copying */ Arr.copy = (array) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion array.slice(); function range$1(start, end, step = 1) { return Array.from(range(start, end, step)); } Arr.range = range$1; function at(...args) { switch (args.length) { case 2: { const [array, index] = args; return pipe(index < 0 ? array.length + index : index).map((normalizedIndex) => normalizedIndex < 0 || normalizedIndex >= array.length ? Optional.none : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Optional.some(array[normalizedIndex])).value; } case 1: { const [index] = args; return (array) => at(array, index); } } } Arr.at = at; /** * Returns the first element of an array wrapped in an Optional. * * This function provides type-safe access to the first element with precise return types: * - For empty arrays: returns `Optional.None` * - For tuples with known first element: returns `Optional.Some<FirstElementType>` * - For non-empty arrays: returns `Optional.Some<ElementType>` * - For general arrays: returns `Optional<ElementType>` * * The function leverages TypeScript's type system to provide the most precise return type * based on the input array type, making it safer than direct indexing. * * @template E The type of elements in the array. * @param array The array to get the first element from. * @returns An Optional containing the first element: * - `Optional.None` if the array is empty * - `Optional.Some<E>` containing the first element if the array is non-empty * * @example * ```typescript * // Empty array - precise None type * const emptyResult = Arr.head([]); // Optional.None * console.log(Optional.isNone(emptyResult)); // true * * // Tuple with known structure - precise Some type * const tupleResult = Arr.head(['first', 'second', 'third'] as const); * // Type: Optional.Some<'first'> * if (Optional.isSome(tupleResult)) { * console.log(tupleResult.value); // 'first' - TypeScript knows exact type * } * * // Non-empty array - guaranteed Some type * const nonEmpty: NonEmptyArray<number> = [10, 20, 30] as NonEmptyArray<number>; * const guaranteedResult = Arr.head(nonEmpty); // Optional.Some<number> * // No need to check - always Some for NonEmptyArray * * // General array - may be Some or None * const generalArray: number[] = [1, 2, 3]; * const maybeResult = Arr.head(generalArray); // Optional<number> * if (Optional.isSome(maybeResult)) { * console.log(`First element: ${maybeResult.value}`); * } else { * console.log('Array is empty'); * } * * // Working with different types * const strings = ['hello', 'world']; * const firstString = Arr.head(strings); // Optional<string> * * const objects = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; * const firstObject = Arr.head(objects); // Optional<{id: number, name: string}> * * // Functional composition * const getFirstElements = (arrays: readonly number[][]) => * arrays.map(Arr.head).filter(Optional.isSome); * * const nestedArrays = [[1, 2], [3, 4], [], [5]]; * const firstElements = getFirstElements(nestedArrays); * // [Optional.Some(1), Optional.Some(3), Optional.Some(5)] * * // Type inference examples * expectType<typeof emptyResult, Optional.None>('='); * expectType<typeof tupleResult, Optional.Some<'first'>>('='); * expectType<typeof guaranteedResult, Optional.Some<number>>('='); * expectType<typeof maybeResult, Optional<number>>('='); * ``` * * @see {@link last} for getting the last element * @see {@link at} for accessing elements at specific indices * @see {@link tail} for getting all elements except the first */ Arr.head = (array) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion (array.length === 0 ? Optional.none : Optional.some(array.at(0))); /** * Returns the last element of an array wrapped in an Optional. * * This function provides type-safe access to the last element with precise return types: * - For empty arrays: returns `Optional.None` * - For tuples with known last element: returns `Optional.Some<LastElementType>` * - For non-empty arrays: returns `Optional.Some<ElementType>` * - For general arrays: returns `Optional<ElementType>` * * The function leverages TypeScript's type system to provide the most precise return type * based on the input array type, making it safer than direct indexing. * * @template E The type of elements in the array. * @param array The array to get the last element from. * @returns An Optional containing the last element: * - `Optional.None` if the array is empty * - `Optional.Some<E>` containing the last element if the array is non-empty * * @example * ```typescript * // Empty array - precise None type * const emptyResult = Arr.last([]); // Optional.None * console.log(Optional.isNone(emptyResult)); // true * * // Tuple with known structure - precise Some type * const tupleResult = Arr.last(['first', 'middle', 'last'] as const); * // Type: Optional.Some<'last'> * if (Optional.isSome(tupleResult)) { * console.log(tupleResult.value); // 'last' - TypeScript knows exact type * } * * // Non-empty array - guaranteed Some type * const nonEmpty: NonEmptyArray<number> = [10, 20, 30] as NonEmptyArray<number>; * const guaranteedResult = Arr.last(nonEmpty); // Optional.Some<number> * // No need to check - always Some for NonEmptyArray * * // General array - may be Some or None * const generalArray: number[] = [1, 2, 3]; * const maybeResult = Arr.last(generalArray); // Optional<number> * if (Optional.isSome(maybeResult)) { * console.log(`Last element: ${maybeResult.value}`); * } else { * console.log('Array is empty'); * } * * // Working with different types * const strings = ['hello', 'world', 'example']; * const lastString = Arr.last(strings); // Optional<string> * * const coordinates = [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}]; * const lastCoordinate = Arr.last(coordinates); // Optional<{x: number, y: number}> * * // Single element arrays * const single = [42]; * const singleResult = Arr.last(single); // Optional<number> containing 42 * * // Functional composition with arrays of arrays * const getLastElements = (arrays: readonly string[][]) => * arrays.map(Arr.last).filter(Optional.isSome); * * const nestedArrays = [['a', 'b'], ['c'], [], ['d', 'e', 'f']]; * const lastElements = getLastElements(nestedArrays); * // [Optional.Some('b'), Optional.Some('c'), Optional.Some('f')] * * // Common pattern: get last element or default * const data = [10, 20, 30]; * const lastOrDefault = Optional.unwrapOr(Arr.last(data), 0); // 30 * const emptyLastOrDefault = Optional.unwrapOr(Arr.last([]), 0); // 0 * * // Type inference examples * expectType<typeof emptyResult, Optional.None>('='); * expectType<typeof tupleResult, Optional.Some<'last'>>('='); * expectType<typeof guaranteedResult, Optional.Some<number>>('='); * expectType<typeof maybeResult, Optional<number>>('='); * ``` * * @see {@link head} for getting the first element * @see {@link at} for accessing elements at specific indices with negative indexing support * @see {@link butLast} for getting all elements except the last */ Arr.last = (array) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion (array.length === 0 ? Optional.none : Optional.some(array.at(-1))); function sliceClamped(...args) { switch (args.length) { case 3: { const [array, start, end] = args; const startClamped = Num.clamp(0, array.length)(start); // Ensure endClamped is not less than startClamped. const endClamped = Num.clamp(startClamped, array.length)(end); return array.slice(startClamped, endClamped); } case 2: { const [start, end] = args; return (array) => sliceClamped(array, start, end); } } } Arr.sliceClamped = sliceClamped; /** * Returns all elements of an array except the first one. * @template E The type of the array (can be a tuple for more precise typing). * @param array The input array. * @returns A new array containing all elements except the first. The type is inferred as `List.Tail<T>`. * @example * ```ts * Arr.tail([1, 2, 3] as const); // [2, 3] * Arr.tail([1] as const); // [] * Arr.tail([]); // [] * ``` */ Arr.tail = (array) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion array.slice(1); /** * Returns all elements of an array except the last one. * @template E The type of the array (can be a tuple for more precise typing). * @param array The input array. * @returns A new array containing all elements except the last. The type is inferred as `List.ButLast<T>`. * @example * ```ts * Arr.butLast([1, 2, 3] as const); // [1, 2] * Arr.butLast([1] as const); // [] * Arr.butLast([]); // [] * ``` */ Arr.butLast = (array) => // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion (Arr.isEmpty(array) ? [] : array.slice(0, -1)); function take(...args) { switch (args.length) { case 2: { const [array, num] = args; return sliceClamped(array, 0, num); } case 1: { const [num] = args; return (array) => take(array, num); } } } Arr.take = take; function takeLast(...args) { switch (args.length) { case 2: { const [array, num] = args; return sliceClamped(array, Uint32.sub(Arr.size(array), num), Arr.size(array)); } case 1: { const [num] = args; return (array) => takeLast(array, num); } } } Arr.takeLast = takeLast; function skip(...args) { switch (args.length) { case 2: { const [array, num] = args; return sliceClamped(array, num, Arr.size(array)); } case 1: { const [num] = args; return (array) => skip(array, num); } } } Arr.skip = skip; function skipLast(...args) { switch (args.length) { case 2: { const [array, num] = args; return sliceClamped(array, 0, Uint32.sub(Arr.size(array), num)); } case 1: { const [num] = args; return (array) => skipLast(array, num); } } } Arr.skipLast = skipLast; function set(...args) { switch (args.length) { case 3: { const [array, index, newValue] = args; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return array.with(index, newValue); } case 2: { const [index, newValue] = args; return (array) => set(array, index, newValue); } } } Arr.set = set; function toUpdated(...args) { switch (args.length) { case 3: { const [array, index, updater] = args; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-type-assertion return array.with(index, updater(array[index])); } case 2: { const [index, updater] = args; return (array) => toUpdated(array, index, updater); } } } Arr.toUpdated = toUpdated; function toInserted(...args) { switch (args.length) { case 3: { const [array, index, newValue] = args; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return array.toSpliced(index, 0, newValue); } case 2: { const [index, newValue] = args; return (array) => toInserted(array, index, newValue); } } } Arr.toInserted = toInserted; function toRemoved(...args) { switch (args.length) { case 2: { const [array, index] = args; return array.toSpliced(index, 1); } case 1: { const [index] = args; return (array) => toRemoved(array, index); } } } Arr.toRemoved = toRemoved; function toPushed(...args) { switch (args.length) { case 2: { const [array, newValue] = args; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return array.toSpliced(array.length, 0, newValue); } case 1: { const [newValue] = args; return (array) => toPushed(array, newValue); } } } Arr.toPushed = toPushed; function toUnshifted(...args) { switch (args.length) { case 2: { const [array, newValue] = args; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return array.toSpliced(0, 0, newValue); } case 1: { const [newValue] = args; return (array) => toUnshifted(array, newValue); } } } Arr.toUnshifted = toUnshifted; function toFilled(...args) { switch (args.length) { case 2: { const [array, value] = args; return Arr.create(asPositiveUint32(array.length), value); } case 1: { const [value] = args; return (array) => toFilled(array, value); } } } Arr.toFilled = toFilled; function toRangeFilled(...args) { switch (args.length) { case 3: { const [array, value, [start, end]] = args; const mut_cp = castMutable(Arr.copy(array)); mut_cp.fill(value, start, end); return mut_cp; } case 2: { const [value, fillRange] = args; return (array) => toRangeFilled(array, value, fillRange); } } } Arr.toRangeFilled = toRangeFilled; function find(...args) { switch (args.length) { case 2: { const [array, predicate] = args; const foundIndex = array.findIndex( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion predicate); return foundIndex === -1 ? Optional.none : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Optional.some(array[foundIndex]); } case 1: { const [predicate] = args; return (array) => find(array, predicate); } } } Arr.find = find; function findLast(...args) { switch (args.length) { case 2: { const [array, predicate] = args; const foundIndex = array.findLastIndex( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion predicate); return foundIndex === -1 ? Optional.none : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Optional.some(array[foundIndex]); } case 1: { const [predicate] = args; return (array) => findLast(array, predicate); } } } Arr.findLast = findLast; function findIndex(...args) { switch (args.length) { case 2: { const [array, predicate] = args; return pipe(array.findIndex( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion predicate)).map((idx) => (idx >= 0 ? asUint32(idx) : -1)).value; } case 1: { const [predicate] = args; return (array) => findIndex(array, predicate); } } } Arr.findIndex = findIndex; function findLastIndex(...args) { switch (args.length) { case 2: { const [array, predicate] = args; return pipe(array.findLastIndex( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion predicate)).map((idx) => (idx >= 0 ? asUint32(idx) : -1)).value; } case 1: { const [predicate] = args; return (array) => findLastIndex(array, predicate); } } } Arr.findLastIndex = findLastIndex; function indexOf(...args) { switch (args.length) { case 2: { const [array, searchElement] = args; const index = array.indexOf(searchElement); return index !== -1 ? asUint32(index) : -1; } case 1: { const [searchElement] = args; return (array) => indexOf(array, searchElement); } } } Arr.indexOf = indexOf; function indexOfFrom(...args) { switch (args.length) { case 3: { const [array, searchElement, fromIndex] = args; const index = array.indexOf(searchElement, fromIndex); return index !== -1 ? asUint32(index) : -1; } case 2: { const [searchElement, fromIndex] = args; return (array) => indexOfFrom(array, searchElement, fromIndex); } } } Arr.indexOfFrom = indexOfFrom; function lastIndexOf(...args) { switch (args.length) { case 2: { const [array, searchElement] = args; const index = array.lastIndexOf(searchElement); return index !== -1 ? asUint32(index) : -1; } case 1: { const [searchElement] = args; return (array) => lastIndexOf(array, searchElement); } } } Arr.lastIndexOf = lastIndexOf; function lastIndexOfFrom(...args) { switch (args.length) { case 3: { const [array, searchElement, fromIndex] = args; const index = array.lastIndexOf(searchElement, fromIndex); return index !== -1 ? asUint32(index) : -1; } case 2: { const [searchElement, fromIndex] = args; return (array) => lastIndexOfFrom(array, searchElement, fromIndex); } } } Arr.lastIndexOfFrom = lastIndexOfFrom; function every(...args) { switch (args.length) { case 2: { const [array, predicate] = args; return array.every((a, i) => predicate(a, asUint32(i))); } case 1: { const [predicate] = args; return (array) => every(array, predicate); } } } Arr.every = every; function some(...args) { switch (args.length) { case 2: { const [array, predicate] = args; return array.some((a, i) => predicate(a, asUint32(i))); } case 1: { const [predicate] = args; return (array) => some(array, predicate); } } } Arr.some = some; function foldl(...args) { switch (args.length) { case 3: { const [array, callbackfn, initialValue] = args; return array.reduce((prev, curr, index) => callbackfn(prev, curr, asUint32(index)), initialValue); } case 2: { const [callbackfn, initialValue] = args; return (array) => foldl(array, callbackfn, initialValue); } } } Arr.foldl = foldl; function foldr(...args) { switch (args.length) { case 3: { const [array, callbackfn, initialValue] = args; return array.reduceRight((prev, curr, index) => callbackfn(prev, curr, asUint32(index)), initialValue); } case 2: { const [callbackfn, initialValue] = args; return (array) => foldr(array, callbackfn, initialValue); } } } Arr.foldr = foldr; function min(array, comparator) { if (!Arr.isNonEmpty(array)) { return Optional.none; } const cmp = comparator ?? ((x, y) => Num.from(x) - Num.from(y)); return Optional.some(array.reduce((currentMin, curr) => (cmp(curr, currentMin) < 0 ? curr : currentMin), array[0])); } Arr.min = min; function max(array, comparator) { const cmp = comparator ?? ((x, y) => Num.from(x) - Num.from(y)); // Find max by finding min with an inverted comparator return min(array, (x, y) => -cmp(x, y)); } Arr.max = max; function minBy(array, comparatorValueMapper, comparator) { return min(array, (x, y) => comparator === undefined ? Num.from(comparatorValueMapper(x)) - Num.from(comparatorValueMapper(y)) : comparator(comparatorValueMapper(x), comparatorValueMapper(y))); } Arr.minBy = minBy; function maxBy(array, comparatorValueMapper, comparator) { return max(array, (x, y) => comparator === undefined ? Num.from(comparatorValueMapper(x)) - Num.from(comparatorValueMapper(y)) : comparator(comparatorValueMapper(x), comparatorValueMapper(y))); } Arr.maxBy = maxBy; function count(...args) { switch (args.length) { case 2: { const [array, predicate] = args; return array.reduce((acc, curr, index) => predicate(curr, asUint32(index)) ? Uint32.add(acc, 1) : acc, asUint32(0)); } case 1: { const [predicate] = args; return (array) => count(array, predicate); } } } Arr.count = count; function countBy(...args) { switch (args.length) { case 2: { const [array, grouper] = args; const mut_groups = new Map(); for (const [index, e] of array.entries()) { const key = grouper(e, asUint32(index)); const curr = mut_groups.get(key) ?? 0; mut_groups.set(key, asUint32(curr + 1)); } return IMap.create(mut_groups); } case 1: { const [grouper] = args; return (array) => countBy(array, grouper); } } } Arr.countBy = countBy; function sum(array) { return array.reduce((prev, curr) => prev + curr, 0); } Arr.sum = sum; function join(...args) { switch (args.length) { case 0: return (array) => joinImpl(array, undefined); case 1: { const [arg] = args;