ts-data-forge
Version:
[](https://www.npmjs.com/package/ts-data-forge) [](https://www.npmjs.com/package/ts-data-forge) [ • 183 kB
text/typescript
import { IMap } from '../collections/index.mjs';
import { expectType } from '../expect-type.mjs';
import { Optional, pipe, Result } from '../functional/index.mjs';
import { isString, isUndefined } from '../guard/index.mjs';
import { range as rangeIterator } from '../iterator/index.mjs';
import { asPositiveUint32, asUint32, Num, Uint32 } from '../number/index.mjs';
import { castMutable, tp, unknownToString } from '../others/index.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.
*/
export namespace Arr {
type ArrayIndex<Ar extends readonly unknown[]> =
IsFixedLengthList<Ar> extends true ? IndexOfTuple<Ar> : SizeType.Arr;
type ArgArrayIndex<Ar extends readonly unknown[]> =
IsFixedLengthList<Ar> extends true ? IndexOfTuple<Ar> : SizeType.ArgArr;
type ArgArrayIndexWithNegative<Ar extends readonly unknown[]> =
IsFixedLengthList<Ar> extends true
? IndexOfTuple<[...Ar, 0]> | NegativeIndexOfTuple<Ar>
: SizeType.ArgArrWithNegative;
/**
* 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
*/
export const size = <Ar extends readonly unknown[]>(
array: Ar,
): Ar extends NonEmptyArray<unknown>
? IntersectBrand<PositiveNumber, SizeType.Arr>
: SizeType.Arr =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
array.length as never;
// 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
* ```
*/
export const isArray = <E,>(value: E): value is FilterArray<E> =>
Array.isArray(value);
type FilterArray<T> = T extends T
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
BoolOr<TypeEq<T, unknown>, TypeEq<T, any>> extends true
? Cast<readonly unknown[], T>
: T extends readonly unknown[]
? T
: never // Exclude non-array types
: never;
type Cast<A, B> = A extends B ? A : never;
// 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
*/
export const isEmpty = <E,>(array: readonly E[]): array is readonly [] =>
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
*/
export const isNonEmpty = <E,>(
array: readonly E[],
): array is NonEmptyArray<E> => 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
* ```
*/
export const isArrayOfLength = <E, N extends SizeType.ArgArr>(
array: readonly E[],
len: N,
): array is ArrayOfLength<N, E> => 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
* ```
*/
export const isArrayAtLeastLength = <E, N extends SizeType.ArgArr>(
array: readonly E[],
len: N,
): array is ArrayAtLeastLen<N, E> => 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
* ```
*/
export const indexIsInRange = <E,>(
array: readonly E[],
index: SizeType.ArgArr,
): boolean => 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[]>('=');
* ```
*/
export const zeros = <N extends SizeType.ArgArr>(
len: N,
): N extends SmallUint
? ArrayOfLength<N, 0>
: N extends SizeType.ArgArrPositive
? NonEmptyArray<0>
: readonly 0[] =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Array.from<0>({ length: len }).fill(0) as never;
/**
* 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]>('=');
* ```
*/
export const seq = <N extends SizeType.ArgArr>(
len: N,
): N extends SmallUint
? Seq<N>
: N extends SizeType.ArgArrPositive
? NonEmptyArray<SizeType.Arr>
: readonly SizeType.Arr[] =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Array.from({ length: len }, (_, i) => i) as never;
/**
* 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
*/
export const create = <const V, N extends SizeType.ArgArr>(
len: N,
init: V,
): N extends SmallUint
? ArrayOfLength<N, V>
: N extends SizeType.ArgArrPositive
? NonEmptyArray<V>
: readonly V[] =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
Array.from({ length: Math.max(0, len) }, () => init) as never;
/**
* 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
* });
* ```
*/
export const generate = <T,>(
generatorFn: () => Generator<T, void, unknown>,
): readonly T[] => 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
*/
export const copy = <Ar extends readonly unknown[]>(array: Ar): Ar =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
array.slice() as unknown as Ar;
/**
* @internal
* Helper type for `range` function to represent a sequence of numbers up to N-1.
* `LEQ[N]` would be `0 | 1 | ... | N-1`.
*/
type LT = Readonly<{
[N in SmallUint]: Index<N>;
}>;
/**
* @internal
* This type is used to avoid incorrect type calculation results for unions with `Seq`.
* It computes the type of an array generated by `Arr.range(S, E)`.
* If `S` or `E` is a union type, it falls back to a more general `readonly number[]` type
* to prevent overly complex or incorrect tuple/union types.
* Otherwise, it computes a precise tuple type like `readonly [S, S+1, ..., E-1]`.
* @template S The start of the range (inclusive), constrained to `SmallUint`.
* @template E The end of the range (exclusive), constrained to `SmallUint`.
*/
type RangeList<S extends SmallUint, E extends SmallUint> =
BoolOr<IsUnion<S>, IsUnion<E>> extends true
? readonly RelaxedExclude<LT[E], LT[Min<S>]>[] // Avoid incorrect type calculation for unions with Seq
: List.Skip<S, Seq<E>>;
expectType<RangeList<1, 5>, readonly [1, 2, 3, 4]>('=');
expectType<RangeList<1, 2>, readonly [1]>('=');
expectType<RangeList<1, 1>, readonly []>('=');
expectType<RangeList<1, 1 | 3>, readonly (1 | 2)[]>('=');
expectType<RangeList<1 | 3, 3 | 5>, readonly (1 | 2 | 3 | 4)[]>('=');
expectType<
RangeList<1 | 2 | 3, 5 | 6 | 7>,
readonly (1 | 2 | 3 | 4 | 5 | 6)[]
>('=');
expectType<RangeList<5, 1>, readonly []>('=');
/**
* Creates an array of numbers within a specified range with optional step increment.
*
* This function generates arithmetic sequences with advanced compile-time type inference:
* - When `start` and `end` are {@link SmallUint} literals and `step` is 1 (or omitted), returns a precise tuple type
* - When parameters are runtime values, returns appropriate array types based on sign constraints
* - Empty arrays are returned for invalid ranges (e.g., start ≥ end with positive step)
* - Never throws exceptions - invalid parameters result in empty arrays
*
* **SmallUint Constraint:** The {@link SmallUint} constraint (0-255) enables precise tuple type inference
* for compile-time known ranges. This allows TypeScript to compute exact tuple types like `readonly [1, 2, 3, 4]`
* instead of generic `readonly number[]`.
*
* **Type Inference Behavior:**
* - Literal {@link SmallUint} values with step=1 → precise tuple type (`RangeList<S, E>`)
* - Non-negative parameters → `readonly SafeUint[]`
* - Mixed signs or negative parameters → `readonly SafeInt[]`
* - Runtime values → lose precise typing but maintain safety
*
* @template S The type of the start value. When a {@link SmallUint} literal (0-255), enables precise tuple typing.
* @template E The type of the end value. When a {@link SmallUint} literal (0-255), enables precise tuple typing.
* @param start The start of the range (inclusive). Must be a safe integer. Supports:
* - **Literal {@link SmallUint}:** Enables precise tuple types (0-255)
* - **Runtime {@link SafeInt}:** Fallback to general array types
* - **Negative values:** Supported for countdown sequences
* @param end The end of the range (exclusive). Must be a safe integer. Supports:
* - **Literal {@link SmallUint}:** Enables precise tuple types (0-255)
* - **Runtime {@link SafeInt}:** Fallback to general array types
* - **Equal to start:** Results in empty array
* @param step The step increment (default: 1). Must be a non-zero safe integer.
* - **Positive step:** generates increasing sequence from start to end
* - **Negative step:** generates decreasing sequence from start to end
* - **Zero step:** Not allowed (branded type prevents this)
* @returns An immutable array containing the arithmetic sequence. Return type depends on parameters:
* - `RangeList<S, E>` (precise tuple like `readonly [1, 2, 3, 4]`) when `S` and `E` are {@link SmallUint} literals and step is 1
* - `readonly SafeUint[]` when all parameters are non-negative
* - `readonly SafeInt[]` for general integer ranges including negative values
*
* @example
* ```typescript
* // Compile-time known ranges with step=1 produce precise tuple types
* const range1to4 = Arr.range(1, 5); // readonly [1, 2, 3, 4]
* const range0to2 = Arr.range(0, 3); // readonly [0, 1, 2]
* const emptyRange = Arr.range(5, 5); // readonly []
* const reverseEmpty = Arr.range(5, 1); // readonly [] (invalid with positive step)
*
* // SmallUint constraint examples (0-255 for precise typing)
* const small = Arr.range(0, 10); // readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
* const maxSmall = Arr.range(250, 255); // readonly [250, 251, 252, 253, 254]
* const beyondSmall = Arr.range(0, 300); // readonly SafeUint[] (loses precision)
*
* // Custom step increments
* const evens = Arr.range(0, 10, 2); // readonly SafeUint[] -> [0, 2, 4, 6, 8]
* const odds = Arr.range(1, 10, 2); // readonly SafeUint[] -> [1, 3, 5, 7, 9]
* const countdown = Arr.range(5, 0, -1); // readonly SafeInt[] -> [5, 4, 3, 2, 1]
* const bigStep = Arr.range(0, 20, 5); // readonly SafeUint[] -> [0, 5, 10, 15]
*
* // Edge cases that return empty arrays
* const singleElement = Arr.range(3, 4); // readonly [3]
* const invalidRange = Arr.range(10, 5, 2); // readonly [] (start > end with positive step)
* const invalidReverse = Arr.range(1, 10, -1); // readonly [] (start < end with negative step)
* const zeroRange = Arr.range(42, 42); // readonly [] (start equals end)
*
* // Runtime ranges lose precise typing but maintain safety
* const dynamicStart = Math.floor(Math.random() * 10) as SafeInt;
* const dynamicEnd = (dynamicStart + 5) as SafeInt;
* const dynamicRange = Arr.range(dynamicStart, dynamicEnd); // readonly SafeInt[]
*
* // Negative numbers and mixed signs
* const negativeRange = Arr.range(-5, 5); // readonly SafeInt[] -> [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
* const negativeCountdown = Arr.range(0, -5, -1); // readonly SafeInt[] -> [0, -1, -2, -3, -4]
*
* // Useful for generating index ranges and iteration
* const indices = Arr.range(0, 10); // readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
* const reversedIndices = Arr.range(9, -1, -1); // readonly SafeInt[] -> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
*
* // Functional programming patterns
* const squares = Arr.range(1, 6).map(x => x * x); // [1, 4, 9, 16, 25]
* const fibonacci = Arr.range(0, 10).reduce((acc, _, i) => {
* if (i <= 1) return [...acc, i];
* return [...acc, acc[i-1] + acc[i-2]];
* }, [] as number[]); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
*
* // Type inference examples showing precise vs general types
* expectType<typeof range1to4, readonly [1, 2, 3, 4]>('='); // Precise tuple
* expectType<typeof emptyRange, readonly []>('='); // Precise empty tuple
* expectType<typeof evens, readonly SafeUint[]>('='); // General positive array
* expectType<typeof countdown, readonly SafeInt[]>('='); // General integer array
* expectType<typeof negativeRange, readonly SafeInt[]>('='); // General integer array
* expectType<typeof small, readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>('='); // Precise tuple
* expectType<typeof beyondSmall, readonly SafeUint[]>('='); // General array (beyond SmallUint)
* ```
*
* @throws Never throws - invalid ranges simply return empty arrays
* @see {@link seq} for creating sequences starting from 0
* @see {@link SmallUint} for understanding the constraint that enables precise typing
* @see {@link SafeInt} and {@link SafeUint} for the safe integer types used
*/
export function range<S extends SmallUint, E extends SmallUint>(
start: S,
end: E,
step?: 1,
): RangeList<S, E>;
export function range(
start: SafeUintWithSmallInt,
end: SafeUintWithSmallInt,
step?: PositiveSafeIntWithSmallInt,
): readonly SafeUint[];
export function range(
start: SafeIntWithSmallInt,
end: SafeIntWithSmallInt,
step?: NonZeroSafeIntWithSmallInt,
): readonly SafeInt[];
export function range(
start: SafeIntWithSmallInt,
end: SafeIntWithSmallInt,
step: NonZeroSafeIntWithSmallInt = 1,
): readonly SafeInt[] {
return Array.from(rangeIterator(start, end, step));
}
// element access
/**
* Safely retrieves an element at a given index from an array, returning an {@link Optional}.
*
* This function provides type-safe array access with support for negative indexing
* (e.g., -1 for the last element). Unlike direct array access which can return
* `undefined` for out-of-bounds indices, this function always returns a well-typed
* {@link Optional} that explicitly represents the possibility of absence.
*
* **Negative Indexing:** Negative indices count from the end of the array:
* - `-1` refers to the last element
* - `-2` refers to the second-to-last element, etc.
*
* **Curried Usage:** This function supports currying - when called with only an index, it returns
* a function that can be applied to arrays, making it ideal for use in pipe operations.
*
* **Optional Return Type:** The return type is always {@link Optional}<E> which provides:
* - Type-safe access without `undefined` in your business logic
* - Explicit handling of \"not found\" cases
* - Composable error handling with {@link Optional} utilities
*
* @template E The type of elements in the array.
* @param array The array to access (when using direct call syntax).
* @param index The index to access. Must be a branded `SizeType.ArgArr` (safe integer). Can be:
* - **Positive integer:** 0-based index from the start (0, 1, 2, ...)
* - **Negative integer:** index from the end (-1 is last element, -2 is second-to-last, etc.)
* - **Out of bounds:** any index beyond array bounds returns {@link Optional.None}
* @returns An {@link Optional}<E> containing:
* - {@link Optional.Some}<E> with the element if the index is valid
* - {@link Optional.None} if the index is out of bounds (including empty arrays)
*
* @example
* ```typescript
* // Direct usage with positive indices
* const fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry'];
* const first = Arr.at(fruits, 0); // Optional.Some('apple')
* const third = Arr.at(fruits, 2); // Optional.Some('cherry')
* const outOfBounds = Arr.at(fruits, 10); // Optional.None
*
* // Negative indexing (accessing from the end)
* const last = Arr.at(fruits, -1); // Optional.Some('elderberry')
* const secondLast = Arr.at(fruits, -2); // Optional.Some('date')
* const negativeOutOfBounds = Arr.at(fruits, -10); // Optional.None
*
* // Edge cases
* const emptyResult = Arr.at([], 0); // Optional.None
* const negativeOnEmpty = Arr.at([], -1); // Optional.None
* const singleElement = Arr.at(['only'], 0); // Optional.Some('only')
* const singleNegative = Arr.at(['only'], -1); // Optional.Some('only')
*
* // Safe access pattern with type-safe unwrapping
* const maybeElement = Arr.at(fruits, 2);
* if (Optional.isSome(maybeElement)) {
* console.log(`Found: ${maybeElement.value}`); // Type-safe access, no undefined
* } else {
* console.log('Index out of bounds');
* }
*
* // Alternative unwrapping with default
* const elementOrDefault = Optional.unwrapOr(Arr.at(fruits, 100), 'not found');
* console.log(elementOrDefault); // 'not found'
*
* // Curried usage for functional composition
* const getSecondElement = Arr.at(1);
* const getLastElement = Arr.at(-1);
* const getMiddleElement = Arr.at(2);
*
* const nestedArrays = [
* [10, 20, 30, 40],
* [50, 60],
* [70]
* ];
* const secondElements = nestedArrays.map(getSecondElement);
* // [Optional.Some(20), Optional.None, Optional.None]
*
* const lastElements = nestedArrays.map(getLastElement);
* // [Optional.Some(40), Optional.Some(60), Optional.Some(70)]
*
* // Pipe composition for data processing
* const processArray = (arr: readonly string[]) => pipe(arr)
* .map(getSecondElement)
* .map(opt => Optional.map(opt, s => s.toUpperCase()))
* .map(opt => Optional.unwrapOr(opt, 'MISSING'))
* .value;
*
* console.log(processArray(['a', 'b', 'c'])); // 'B'
* console.log(processArray(['x'])); // 'MISSING'
*
* // Advanced curried usage with transformation pipelines
* const extractAndProcess = pipe([
* ['hello', 'world', 'typescript'],
* ['functional', 'programming'],
* ['type', 'safety', 'matters', 'most']
* ])
* .map(arr => arr.map(getSecondElement))
* .map(opts => opts.map(opt => Optional.unwrapOr(opt, '[missing]')))
* .value;
* // [['world'], ['[missing]'], ['safety']]
*
* // Type inference examples
* expectType<typeof first, Optional<string>>('=');
* expectType<typeof getSecondElement, <T>(array: readonly T[]) => Optional<T>>('=');
* expectType<typeof negativeOutOfBounds, Optional<string>>('=');
* ```
*
* @see {@link head} for getting the first element specifically
* @see {@link last} for getting the last element specifically
* @see {@link Optional} for working with the returned Optional values
* @see {@link Optional.unwrapOr} for safe unwrapping with defaults
* @see {@link Optional.map} for transforming Optional values
*/
export function at<Ar extends readonly unknown[]>(
array: Ar,
index: ArgArrayIndexWithNegative<Ar>,
): Optional<Ar[number]>;
// Curried version
export function at(
index: SizeType.ArgArrWithNegative,
): <E>(array: readonly E[]) => Optional<E>;
export function at<E>(
...args:
| readonly [array: readonly E[], index: SizeType.ArgArrWithNegative]
| readonly [index: SizeType.ArgArrWithNegative]
): Optional<E> | ((array: readonly E[]) => Optional<E>) {
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);
}
}
}
/**
* 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
*/
export const head = <Ar extends readonly unknown[]>(
array: Ar,
): Ar extends readonly []
? Optional.None
: Ar extends readonly [infer E, ...unknown[]]
? Optional.Some<E>
: Ar extends NonEmptyArray<infer E>
? Optional.Some<E>
: Optional<Ar[number]> =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(array.length === 0 ? Optional.none : Optional.some(array.at(0))) as never;
/**
* 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
*/
export const last = <Ar extends readonly unknown[]>(
array: Ar,
): Ar extends readonly []
? Optional.None
: Ar extends readonly [...unknown[], infer E]
? Optional.Some<E>
: Ar extends NonEmptyArray<infer E>
? Optional.Some<E>
: Optional<Ar[number]> =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(array.length === 0 ? Optional.none : Optional.some(array.at(-1))) as never;
// slicing
/**
* Slices an array with automatically clamped start and end indices for safe bounds handling.
*
* This function provides a safer alternative to `Array.slice()` by automatically clamping
* the start and end indices to valid bounds, preventing out-of-bounds access and ensuring
* consistent behavior regardless of input values.
*
* **Clamping Behavior:**
* - `start` is clamped to `[0, array.length]`
* - `end` is clamped to `[clampedStart, array.length]` (ensuring end ≥ start)
* - Invalid ranges (start > end after clamping) return empty arrays
* - Negative indices are clamped to 0, large indices are clamped to array.length
*
* **Curried Usage:** This function supports currying - when called with only start and end
* indices, it returns a function that can be applied to arrays.
*
* @template E The type of elements in the array.
* @param array The array to slice (when using direct call syntax).
* @param start The start index for the slice (inclusive). Will be clamped to valid bounds.
* @param end The end index for the slice (exclusive). Will be clamped to valid bounds.
* @returns A new immutable array containing the sliced elements. Always returns a valid array,
* never throws for out-of-bounds indices.
*
* @example
* ```typescript
* const data = [10, 20, 30, 40, 50];
*
* // Normal slicing
* const middle = Arr.sliceClamped(data, 1, 4); // [20, 30, 40]
* const beginning = Arr.sliceClamped(data, 0, 2); // [10, 20]
* const end = Arr.sliceClamped(data, 3, 5); // [40, 50]
*
* // Automatic clamping for out-of-bounds indices
* const clampedStart = Arr.sliceClamped(data, -10, 3); // [10, 20, 30] (start clamped to 0)
* const clampedEnd = Arr.sliceClamped(data, 2, 100); // [30, 40, 50] (end clamped to length)
* const bothClamped = Arr.sliceClamped(data, -5, 100); // [10, 20, 30, 40, 50] (entire array)
*
* // Invalid ranges become empty arrays
* const emptyReversed = Arr.sliceClamped(data, 4, 1); // [] (start > end after clamping)
* const emptyAtEnd = Arr.sliceClamped(data, 5, 10); // [] (start at end of array)
*
* // Edge cases
* const emptyArray = Arr.sliceClamped([], 0, 5); // [] (empty input)
* const singleElement = Arr.sliceClamped([42], 0, 1); // [42]
* const fullCopy = Arr.sliceClamped(data, 0, data.length); // [10, 20, 30, 40, 50]
*
* // Curried usage for functional composition
* const takeFirst3 = Arr.sliceClamped(0, 3);
* const getMiddle2 = Arr.sliceClamped(1, 3);
*
* const arrays = [
* [1, 2, 3, 4, 5],
* [10, 20],
* [100, 200, 300, 400, 500, 600]
* ];
*
* const first3Elements = arrays.map(takeFirst3);
* // [[1, 2, 3], [10, 20], [100, 200, 300]]