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.

493 lines (432 loc) 17.4 kB
import { expectType } from '../expect-type.mjs'; import { Arr } from './array-utils.mjs'; describe('Arr validations', () => { describe('isArray', () => { test('should return true for arrays', () => { expect(Arr.isArray([1, 2, 3])).toBe(true); expect(Arr.isArray([])).toBe(true); expect(Arr.isArray(['a', 'b'])).toBe(true); }); test('should return false for non-arrays', () => { expect(Arr.isArray('hello')).toBe(false); expect(Arr.isArray(123)).toBe(false); expect(Arr.isArray(null)).toBe(false); expect(Arr.isArray(undefined)).toBe(false); expect(Arr.isArray({})).toBe(false); expect(Arr.isArray(new Set())).toBe(false); }); test('should refine union types correctly', () => { const processValue = ( value: string | readonly number[] | null, ): number => { if (Arr.isArray(value)) { // value should be typed as number[] expectType<typeof value, readonly number[]>('='); return value.length; } return 0; }; expect(processValue([1, 2, 3])).toBe(3); expect(processValue('hello')).toBe(0); expect(processValue(null)).toBe(0); }); test('should work with readonly arrays', () => { const readonlyArray: readonly number[] = [1, 2, 3]; if (Arr.isArray(readonlyArray)) { expectType<typeof readonlyArray, readonly number[]>('='); expect(readonlyArray).toHaveLength(3); } }); test('should work with mutable arrays', () => { const mutableArray: number[] = [1, 2, 3]; if (Arr.isArray(mutableArray)) { expectType<typeof mutableArray, number[]>('='); expect(mutableArray).toHaveLength(3); } }); test('should exclude impossible array types from unions', () => { const checkUnion = ( value: string | boolean | readonly number[] | Readonly<{ a: number }>, ): number => { if (Arr.isArray(value)) { // Only number[] should remain expectType<typeof value, readonly number[]>('='); return value.length; } // Non-array types expectType<typeof value, string | boolean | Readonly<{ a: number }>>( '=', ); return -1; }; expect(checkUnion([1, 2])).toBe(2); expect(checkUnion('test')).toBe(-1); expect(checkUnion(true)).toBe(-1); expect(checkUnion({ a: 1 })).toBe(-1); }); test('should exclude impossible array types from unions (including unknown)', () => { const checkUnion = ( value: | string | boolean | readonly number[] | Readonly<{ a: number }> // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | unknown // eslint-disable-next-line @typescript-eslint/no-restricted-types | object, ): number => { if (Arr.isArray(value)) { // Only number[] should remain expectType<typeof value, readonly unknown[]>('='); return value.length; } // Non-array types expectType< typeof value, | string | boolean | Readonly<{ a: number }> // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | unknown // eslint-disable-next-line @typescript-eslint/no-restricted-types | object >('='); return -1; }; expect(checkUnion([1, 2])).toBe(2); expect(checkUnion('test')).toBe(-1); expect(checkUnion(true)).toBe(-1); expect(checkUnion({ a: 1 })).toBe(-1); }); test('should return true for arrays (additional)', () => { expect(Arr.isArray([])).toBe(true); expect(Arr.isArray([1, 2, 3])).toBe(true); expect(Arr.isArray(['a', 'b'])).toBe(true); }); test('should return false for non-arrays (additional)', () => { expect(Arr.isArray('string')).toBe(false); expect(Arr.isArray(123)).toBe(false); expect(Arr.isArray({})).toBe(false); expect(Arr.isArray(null)).toBe(false); expect(Arr.isArray(undefined)).toBe(false); }); test('should work as type guard (additional)', () => { const value: unknown = [1, 2, 3]; if (Arr.isArray(value)) { expectType<typeof value, readonly unknown[]>('='); expect(value).toHaveLength(3); } }); test('should handle array-like objects', () => { const arrayLike = { 0: 'a', 1: 'b', length: 2 }; expect(Arr.isArray(arrayLike)).toBe(false); }); describe('comprehensive type guard tests', () => { test('should narrow unknown type to array', () => { const value: unknown = [1, 2, 3]; if (Arr.isArray(value)) { expectType<typeof value, readonly unknown[]>('='); } else { expectType<typeof value, unknown>('='); } }); test('should handle any type', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const value: any = [1, 2, 3]; if (Arr.isArray(value)) { expectType<typeof value, readonly unknown[]>('='); } }); test('should work with nested arrays', () => { const nested: readonly (readonly number[])[] = [[1], [2], [3]]; if (Arr.isArray(nested)) { expectType<typeof nested, readonly (readonly number[])[]>('='); } }); test('should distinguish between array and tuple types', () => { const tuple: readonly [1, 2, 3] = [1, 2, 3]; if (Arr.isArray(tuple)) { expectType<typeof tuple, readonly [1, 2, 3]>('='); } }); test('should work with empty tuple type', () => { const emptyTuple: readonly [] = []; if (Arr.isArray(emptyTuple)) { expectType<typeof emptyTuple, readonly []>('='); } }); test('should handle union of array types', () => { type MixedArrayUnion = | readonly string[] | readonly number[] | readonly boolean[]; const mixedArray: MixedArrayUnion = [1, 2, 3]; if (Arr.isArray(mixedArray)) { expectType<typeof mixedArray, MixedArrayUnion>('<='); } }); test('should work with generic function', () => { const processGeneric = <T,>(value: T | readonly number[]): number => { if (Arr.isArray(value)) { // Type is narrowed to array type within this block return value.length; } return 0; }; expect(processGeneric([1, 2, 3])).toBe(3); expect(processGeneric('hello')).toBe(0); }); test('should handle never type correctly', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const neverValue = undefined as never; if (Arr.isArray(neverValue)) { expectType<typeof neverValue, never>('='); } }); test('should work with conditional types', () => { type ArrayOrValue<T> = T extends readonly unknown[] ? T : readonly T[]; const makeArray = <T,>(value: T): ArrayOrValue<T> => { if (Arr.isArray(value)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return value as ArrayOrValue<T>; } // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return [value] as ArrayOrValue<T>; }; expect(makeArray([1, 2, 3])).toStrictEqual([1, 2, 3]); expect(makeArray(5)).toStrictEqual([5]); }); test('should handle intersection types', () => { type TaggedArray = readonly number[] & { tag: string }; const tagged = Object.assign([1, 2, 3], { tag: 'test' }) as TaggedArray; if (Arr.isArray(tagged)) { expectType<typeof tagged, TaggedArray>('='); expect(tagged.tag).toBe('test'); } }); test('should work with branded types', () => { type BrandedArray = readonly number[] & Readonly<{ __brand: unknown }>; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const branded = [1, 2, 3] as unknown as BrandedArray; if (Arr.isArray(branded)) { expectType<typeof branded, BrandedArray>('='); } }); test('should handle complex union discrimination', () => { type ComplexUnion = | { type: 'array'; data: readonly string[] } | { type: 'object'; data: Record<string, unknown> } | readonly number[] | string | null; // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types const processComplex = (value: ComplexUnion): number => { if (Arr.isArray(value)) { expectType<typeof value, readonly number[]>('='); return value.length; } if (typeof value === 'string') { expectType<typeof value, string>('='); return value.length; } if (value === null) { expectType<typeof value, null>('='); return 0; } expectType< typeof value, | { type: 'array'; data: readonly string[] } | { type: 'object'; data: Record<string, unknown> } >('='); return -1; }; expect(processComplex([1, 2, 3])).toBe(3); expect(processComplex('test')).toBe(4); expect(processComplex(null)).toBe(0); expect(processComplex({ type: 'array', data: ['a', 'b'] })).toBe(-1); }); test('should preserve literal types in arrays', () => { const literalArray = [1, 2, 3] as const; if (Arr.isArray(literalArray)) { expectType<typeof literalArray, readonly [1, 2, 3]>('='); } }); test('should handle arrays with mixed element types', () => { const mixed: readonly (string | number | boolean)[] = [1, 'two', true]; if (Arr.isArray(mixed)) { expectType<typeof mixed, readonly (string | number | boolean)[]>('='); } }); test('should work with symbol-keyed arrays', () => { const sym = Symbol('test'); const arrWithSymbol = Object.assign([1, 2, 3], { [sym]: 'value' }); if (Arr.isArray(arrWithSymbol)) { expect(arrWithSymbol).toHaveLength(3); } }); }); }); describe('isEmpty', () => { const xs = [1, 2, 3] as const; const result = Arr.isEmpty(xs); expectType<typeof result, boolean>('='); test('case 1', () => { expect(result).toBe(false); }); test('case 2', () => { expect(Arr.isEmpty([])).toBe(true); }); }); describe('isNonEmpty', () => { const xs = [1, 2, 3] as const; const result = Arr.isNonEmpty(xs); expectType<typeof result, boolean>('='); test('case 1', () => { expect(result).toBe(true); }); test('case 2', () => { expect(Arr.isNonEmpty([])).toBe(false); }); }); describe('isArrayOfLength', () => { test('should return true if array has specified length', () => { const arr = [1, 2, 3] as const; expect(Arr.isArrayOfLength(arr, 3)).toBe(true); if (Arr.isArrayOfLength(arr, 3)) { expectType<typeof arr, readonly [1, 2, 3]>('='); } }); test('should return false if array does not have specified length', () => { const arr = [1, 2, 3] as const; expect(Arr.isArrayOfLength(arr, 2)).toBe(false); }); test('should return true for empty array and length 0', () => { const arr = [] as const; expect(Arr.isArrayOfLength(arr, 0)).toBe(true); if (Arr.isArrayOfLength(arr, 0)) { expectType<typeof arr, readonly []>('='); } }); test('should return false for non-empty array and length 0', () => { const arr = [1] as const; expect(Arr.isArrayOfLength(arr, 0)).toBe(false); }); test('should work with unknown array type', () => { const arr: number[] = [1, 2]; expect(Arr.isArrayOfLength(arr, 2)).toBe(true); if (Arr.isArrayOfLength(arr, 2)) { expectType<typeof arr, number[] & ArrayOfLength<2, number>>('='); } expect(Arr.isArrayOfLength(arr, 3)).toBe(false); }); test('should work with unknown readonly array type', () => { const arr: readonly number[] = [1, 2]; expect(Arr.isArrayOfLength(arr, 2)).toBe(true); if (Arr.isArrayOfLength(arr, 2)) { expectType<typeof arr, ArrayOfLength<2, number>>('='); } expect(Arr.isArrayOfLength(arr, 3)).toBe(false); }); test('should return true for arrays of exact length (additional)', () => { expect(Arr.isArrayOfLength([1, 2, 3], 3)).toBe(true); expect(Arr.isArrayOfLength([], 0)).toBe(true); expect(Arr.isArrayOfLength(['a'], 1)).toBe(true); }); test('should return false for arrays of different length (additional)', () => { expect(Arr.isArrayOfLength([1, 2, 3], 2)).toBe(false); expect(Arr.isArrayOfLength([1, 2, 3], 4)).toBe(false); expect(Arr.isArrayOfLength([], 1)).toBe(false); }); test('should work as type guard with exact length (additional)', () => { const array: readonly number[] = [1, 2, 3]; if (Arr.isArrayOfLength(array, 3)) { expectType<typeof array, ArrayOfLength<3, number>>('='); expect(array).toHaveLength(3); } }); }); describe('isArrayAtLeastLength', () => { test('should return true if array length is greater than or equal to specified length', () => { const arr = [1, 2, 3] as const; expect(Arr.isArrayAtLeastLength(arr, 3)).toBe(true); if (Arr.isArrayAtLeastLength(arr, 3)) { expectType<typeof arr, readonly [1, 2, 3]>('='); } expect(Arr.isArrayAtLeastLength(arr, 2)).toBe(true); if (Arr.isArrayAtLeastLength(arr, 2)) { expectType<typeof arr, readonly [1, 2, 3]>('='); } }); test('should return false if array length is less than specified length', () => { const arr = [1, 2, 3] as const; expect(Arr.isArrayAtLeastLength(arr, 4)).toBe(false); }); test('should return true for empty array and length 0', () => { const arr = [] as const; expect(Arr.isArrayAtLeastLength(arr, 0)).toBe(true); if (Arr.isArrayAtLeastLength(arr, 0)) { expectType<typeof arr, readonly []>('='); } }); test('should return false for empty array and positive length', () => { const arr = [] as const; expect(Arr.isArrayAtLeastLength(arr, 1)).toBe(false); }); test('should work with unknown array type', () => { const arr: number[] = [1, 2]; expect(Arr.isArrayAtLeastLength(arr, 2)).toBe(true); if (Arr.isArrayAtLeastLength(arr, 2)) { expectType<typeof arr, number[] & ArrayAtLeastLen<2, number>>('='); } expect(Arr.isArrayAtLeastLength(arr, 1)).toBe(true); if (Arr.isArrayAtLeastLength(arr, 1)) { expectType<typeof arr, number[] & ArrayAtLeastLen<1, number>>('='); } expect(Arr.isArrayAtLeastLength(arr, 3)).toBe(false); }); test('should return true for arrays of at least specified length (additional)', () => { expect(Arr.isArrayAtLeastLength([1, 2, 3], 3)).toBe(true); expect(Arr.isArrayAtLeastLength([1, 2, 3], 2)).toBe(true); expect(Arr.isArrayAtLeastLength([1, 2, 3], 1)).toBe(true); expect(Arr.isArrayAtLeastLength([1, 2, 3], 0)).toBe(true); }); test('should return false for arrays shorter than specified length (additional)', () => { expect(Arr.isArrayAtLeastLength([1, 2, 3], 4)).toBe(false); expect(Arr.isArrayAtLeastLength([], 1)).toBe(false); }); test('should work as type guard for at least length (additional)', () => { const array: readonly number[] = [1, 2, 3]; if (Arr.isArrayAtLeastLength(array, 2)) { expectType<typeof array, ArrayAtLeastLen<2, number>>('='); expect(array.length).toBeGreaterThanOrEqual(2); } }); }); describe('indexIsInRange', () => { test('should return true for valid indices', () => { const array = ['a', 'b', 'c']; expect(Arr.indexIsInRange(array, 0)).toBe(true); expect(Arr.indexIsInRange(array, 1)).toBe(true); expect(Arr.indexIsInRange(array, 2)).toBe(true); }); test('should return false for invalid indices', () => { const array = ['a', 'b', 'c']; expect(Arr.indexIsInRange(array, 3)).toBe(false); expect(Arr.indexIsInRange(array, 10)).toBe(false); }); test('should work with empty array', () => { const empty: readonly string[] = []; expect(Arr.indexIsInRange(empty, 0)).toBe(false); // @ts-expect-error negative indices should not be allowed expect(Arr.indexIsInRange(empty, -1)).toBe(false); }); test('should be type error with floating point indices', () => { const array = [1, 2, 3]; // @ts-expect-error floating point indices should not be allowed expect(Arr.indexIsInRange(array, 1.5)).toBe(true); // JavaScript arrays accept floating point indices // @ts-expect-error floating point indices should not be allowed expect(Arr.indexIsInRange(array, 3.1)).toBe(false); }); }); });