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.

754 lines (503 loc) 21.1 kB
import { expectType } from '../expect-type.mjs'; import { Optional } from './optional/index.mjs'; import { pipe } from './pipe.mjs'; describe('Optional test', () => { describe('isOptional', () => { test('should return true for Some values', () => { assert.isTrue(Optional.isOptional(Optional.some(42))); assert.isTrue(Optional.isOptional(Optional.some('hello'))); assert.isTrue(Optional.isOptional(Optional.some(null))); assert.isTrue(Optional.isOptional(Optional.some(undefined))); }); test('should return true for None value', () => { assert.isTrue(Optional.isOptional(Optional.none)); }); test('should return false for non-Optional values', () => { assert.isFalse(Optional.isOptional(42)); assert.isFalse(Optional.isOptional('hello')); assert.isFalse(Optional.isOptional(null)); assert.isFalse(Optional.isOptional(undefined)); assert.isFalse(Optional.isOptional({})); assert.isFalse(Optional.isOptional({ type: 'fake', value: 42 })); }); }); describe('some', () => { test('should create a Some variant with the provided value', () => { const someNumber = Optional.some(42); assert.isTrue(Optional.isSome(someNumber)); expect(Optional.unwrap(someNumber)).toBe(42); const someString = Optional.some('hello'); assert.isTrue(Optional.isSome(someString)); expect(Optional.unwrap(someString)).toBe('hello'); const someObject = Optional.some({ name: 'Alice', age: 30 }); assert.isTrue(Optional.isSome(someObject)); assert.deepStrictEqual(Optional.unwrap(someObject), { name: 'Alice', age: 30, }); }); test('should preserve const types', () => { expectTypeOf(Optional.some('test' as const)).toEqualTypeOf< Some<'test'> >(); }); }); describe('none', () => { test('should be a singleton None value', () => { assert.isTrue(Optional.isNone(Optional.none)); assert.isFalse(Optional.isSome(Optional.none)); // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression expect(Optional.unwrapOr(Optional.none, undefined)).toBeUndefined(); }); test('should always reference the same instance', () => { expect(Optional.none).toBe(Optional.none); }); }); describe('isSome and isNone', () => { test('should correctly identify Some values', () => { const some = Optional.some(42); assert.isTrue(Optional.isSome(some)); assert.isFalse(Optional.isNone(some)); }); test('should correctly identify None values', () => { const none = Optional.none; assert.isFalse(Optional.isSome(none)); assert.isTrue(Optional.isNone(none)); }); test('should act as type guards', () => { const optional = Optional.some(42) as Optional<number>; if (Optional.isSome(optional)) { expectType<typeof optional, Some<number>>('='); } if (Optional.isNone(optional)) { expectType<typeof optional, None>('<='); } }); }); describe('map', () => { test('should map over Some values', () => { const some = Optional.some(5); const mapped = Optional.map(some, (x) => x * 2); assert.isTrue(Optional.isSome(mapped)); if (Optional.isSome(mapped)) { expect(Optional.unwrap(mapped)).toBe(10); } }); test('should return None for None values', () => { const none = Optional.none; const mapped = Optional.map(none, (x: never) => x * 2); assert.isTrue(Optional.isNone(mapped)); }); test('should support chaining', () => { const result = Optional.map( Optional.map(Optional.some('hello'), (s) => s.toUpperCase()), (s) => s.length, ); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(5); } }); test('should preserve types correctly', () => { const some = Optional.some(42); expectTypeOf(Optional.map(some, (x) => x.toString())).toEqualTypeOf< Optional<string> >(); }); test('should support curried form', () => { const doubler = Optional.map((x: number) => x * 2); const some = Optional.some(5); const mapped = doubler(some); assert.isTrue(Optional.isSome(mapped)); if (Optional.isSome(mapped)) { expect(Optional.unwrap(mapped)).toBe(10); } const none = Optional.none; const mappedNone = doubler(none); assert.isTrue(Optional.isNone(mappedNone)); }); test('should work with pipe when curried', () => { const doubler = Optional.map((x: number) => x * 2); const toStringFn = Optional.map((x: number) => x.toString()); const result = pipe(Optional.some(5)).map(doubler).map(toStringFn).value; assert.isTrue(Optional.isSome(result)); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe('10'); } }); }); describe('unwrap', () => { test('should return the value for Some', () => { expect(Optional.unwrap(Optional.some(42))).toBe(42); expect(Optional.unwrap(Optional.some('hello'))).toBe('hello'); expect(Optional.unwrap(Optional.some(null))).toBeNull(); }); test('should return undefined for None', () => { // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression expect(Optional.unwrapOr(Optional.none, undefined)).toBeUndefined(); }); test('should have correct return types', () => { const someNumber = Optional.some(42); expectTypeOf(Optional.unwrap(someNumber)).toExtend<number | undefined>(); const none = Optional.none; // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression expectTypeOf(Optional.unwrapOr(none, undefined)).toExtend<undefined>(); }); }); describe('unwrapThrow', () => { test('should return the value for Some', () => { expect(Optional.unwrapThrow(Optional.some(42))).toBe(42); expect(Optional.unwrapThrow(Optional.some('hello'))).toBe('hello'); }); test('should throw for None', () => { expect(() => Optional.unwrapThrow(Optional.none)).toThrow( '`unwrapThrow()` has failed because it is `None`', ); }); test('should have correct return types', () => { const someNumber = Optional.some(42); expectTypeOf(Optional.unwrapThrow(someNumber)).toExtend<number>(); }); }); describe('unwrapOr', () => { test('should return the value for Some', () => { expect(Optional.unwrapOr(Optional.some(42), 0)).toBe(42); expect(Optional.unwrapOr(Optional.some('hello'), 'default')).toBe( 'hello', ); }); test('should return the default value for None', () => { expect(Optional.unwrapOr(Optional.none, 0)).toBe(0); expect(Optional.unwrapOr(Optional.none, 'default')).toBe('default'); }); test('should have correct return types', () => { const someNumber = Optional.some(42); expectTypeOf(Optional.unwrapOr(someNumber, 0)).toExtend<number>(); expectTypeOf(Optional.unwrapOr(someNumber, 'default')).toExtend< number | string >(); const none = Optional.none; expectTypeOf(Optional.unwrapOr(none, 'default')).toExtend<string>(); }); test('should support curried form', () => { const unwrapWithDefault = Optional.unwrapOr(42); const someValue = Optional.some(100); const result = unwrapWithDefault(someValue); expect(result).toBe(100); const noneValue = Optional.none; const defaultResult = unwrapWithDefault(noneValue); expect(defaultResult).toBe(42); }); test('should work with pipe when curried', () => { const unwrapWithDefault = Optional.unwrapOr('default'); const someResult = pipe(Optional.some('hello')).map( unwrapWithDefault, ).value; expect(someResult).toBe('hello'); const noneResult = pipe(Optional.none).map(unwrapWithDefault).value; expect(noneResult).toBe('default'); }); }); describe('expectToBe', () => { test('should return the value for Some', () => { const expectNumber = Optional.expectToBe<number>('Expected a number'); expect(expectNumber(Optional.some(42))).toBe(42); }); test('should throw with custom message for None', () => { const expectNumber = Optional.expectToBe<number>('Expected a number'); expect(() => expectNumber(Optional.none)).toThrow('Expected a number'); }); test('should be curried', () => { const expectValidId = Optional.expectToBe<string>('ID is required'); const id1 = Optional.some('user-123'); const id2 = Optional.none; expect(expectValidId(id1)).toBe('user-123'); expect(() => expectValidId(id2)).toThrow('ID is required'); }); test('should support curried form', () => { const getValue = Optional.expectToBe('Value must exist'); const someValue = Optional.some('important data'); const result = getValue(someValue); expect(result).toBe('important data'); const noneValue = Optional.none; expect(() => getValue(noneValue)).toThrow('Value must exist'); }); test('should work with pipe when curried', () => { const expectUser = Optional.expectToBe('User not found'); const someResult = pipe(Optional.some({ name: 'Alice', age: 30 })).map( expectUser, ).value; assert.deepStrictEqual(someResult, { name: 'Alice', age: 30 }); expect(() => pipe(Optional.none).map(expectUser).value).toThrow( 'User not found', ); }); }); describe('type utilities', () => { test('should correctly unwrap types', () => { type SomeNumber = Some<number>; type UnwrappedNumber = Optional.Unwrap<SomeNumber>; expectType<UnwrappedNumber, number>('='); type UnwrappedNone = Optional.Unwrap<None>; expectType<UnwrappedNone, never>('='); }); test('should correctly narrow types', () => { type MaybeNumber = Optional<number>; type OnlySome = Optional.NarrowToSome<MaybeNumber>; expectType<OnlySome, Some<number>>('='); type OnlyNone = Optional.NarrowToNone<MaybeNumber>; expectType<OnlyNone, None>('='); }); }); describe('flatMap', () => { test('should chain operations that return Optional', () => { const parseNumber = (s: string): Optional<number> => { const n = Number(s); return Number.isNaN(n) ? Optional.none : Optional.some(n); }; const result = Optional.flatMap(Optional.some('42'), parseNumber); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(42); } const invalid = Optional.flatMap(Optional.some('abc'), parseNumber); assert.isTrue(Optional.isNone(invalid)); }); test('should return None if input is None', () => { const result = Optional.flatMap(Optional.none, (_: never) => Optional.some(42), ); assert.isTrue(Optional.isNone(result)); }); test('should support chaining multiple flatMaps', () => { const parseNumber = (s: string): Optional<number> => { const n = Number(s); return Number.isNaN(n) ? Optional.none : Optional.some(n); }; const divideBy = (divisor: number) => (n: number): Optional<number> => divisor === 0 ? Optional.none : // eslint-disable-next-line total-functions/no-partial-division Optional.some(n / divisor); const intermediate = Optional.flatMap(Optional.some('100'), parseNumber); const result = Optional.flatMap(intermediate, divideBy(2)); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(50); } }); test('should support curried form', () => { const parseNumber = (s: string): Optional<number> => { const n = Number(s); return Number.isNaN(n) ? Optional.none : Optional.some(n); }; const parser = Optional.flatMap(parseNumber); const result = parser(Optional.some('42')); assert.isTrue(Optional.isSome(result)); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(42); } const invalid = parser(Optional.some('abc')); assert.isTrue(Optional.isNone(invalid)); const noneResult = parser(Optional.none); assert.isTrue(Optional.isNone(noneResult)); }); test('should work with pipe when curried', () => { const parseNumber = (s: string): Optional<number> => { const n = Number(s); return Number.isNaN(n) ? Optional.none : Optional.some(n); }; const doubleIfPositive = (n: number): Optional<number> => n > 0 ? Optional.some(n * 2) : Optional.none; const parser = Optional.flatMap(parseNumber); const doubler = Optional.flatMap(doubleIfPositive); const result = pipe(Optional.some('42')).map(parser).map(doubler).value; assert.isTrue(Optional.isSome(result)); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(84); } }); }); describe('filter', () => { test('should keep Some values that match predicate', () => { const someEven = Optional.some(4); const filtered = Optional.filter(someEven, (x) => x % 2 === 0); if (Optional.isSome(filtered)) { expect(Optional.unwrap(filtered)).toBe(4); } }); test('should return None for Some values that do not match predicate', () => { const someOdd = Optional.some(5); const filtered = Optional.filter(someOdd, (x) => x % 2 === 0); assert.isTrue(Optional.isNone(filtered)); }); test('should return None if input is None', () => { const filtered = Optional.filter(Optional.none, (_: never) => true); assert.isTrue(Optional.isNone(filtered)); }); test('should support curried form', () => { const evenFilter = Optional.filter((x: number) => x % 2 === 0); const someEven = Optional.some(4); const filtered = evenFilter(someEven); assert.isTrue(Optional.isSome(filtered)); if (Optional.isSome(filtered)) { expect(Optional.unwrap(filtered)).toBe(4); } const someOdd = Optional.some(5); const filteredOdd = evenFilter(someOdd); assert.isTrue(Optional.isNone(filteredOdd)); const noneResult = evenFilter(Optional.none); assert.isTrue(Optional.isNone(noneResult)); }); test('should work with pipe when curried', () => { const evenFilter = Optional.filter((x: number) => x % 2 === 0); const positiveFilter = Optional.filter((x: number) => x > 0); const result = pipe(Optional.some(4)) .map(evenFilter) .map(positiveFilter).value; assert.isTrue(Optional.isSome(result)); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(4); } const filtered = pipe(Optional.some(3)).map(evenFilter).value; assert.isTrue(Optional.isNone(filtered)); }); }); describe('orElse', () => { test('should return the first Optional if it is Some', () => { const primary = Optional.some(42); const fallback = Optional.some(100); const result = Optional.orElse(primary, fallback); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe(42); } }); test('should return the alternative if the first is None', () => { const primary = Optional.none; const fallback = Optional.some('default'); const result = Optional.orElse(primary, fallback); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe('default'); } }); test('should return None if both are None', () => { const result = Optional.orElse(Optional.none, Optional.none); assert.isTrue(Optional.isNone(result)); }); test('should support curried form', () => { const fallbackTo = Optional.orElse(Optional.some('fallback')); const someValue = Optional.some('primary'); const result = fallbackTo(someValue); assert.isTrue(Optional.isSome(result)); if (Optional.isSome(result)) { expect(Optional.unwrap(result)).toBe('primary'); } const noneValue = Optional.none; const fallbackResult = fallbackTo(noneValue); assert.isTrue(Optional.isSome(fallbackResult)); if (Optional.isSome(fallbackResult)) { expect(Optional.unwrap(fallbackResult)).toBe('fallback'); } }); test('should work with pipe when curried', () => { const fallbackTo = Optional.orElse(Optional.some('backup')); const someResult = pipe(Optional.some('original')).map(fallbackTo).value; assert.isTrue(Optional.isSome(someResult)); if (Optional.isSome(someResult)) { expect(Optional.unwrap(someResult)).toBe('original'); } const noneResult = pipe(Optional.none).map(fallbackTo).value; assert.isTrue(Optional.isSome(noneResult)); if (Optional.isSome(noneResult)) { expect(Optional.unwrap(noneResult)).toBe('backup'); } }); }); describe('zip', () => { test('should combine two Some values into a tuple', () => { const a = Optional.some(1); const b = Optional.some('hello'); const zipped = Optional.zip(a, b); if (Optional.isSome(zipped)) { assert.deepStrictEqual(Optional.unwrap(zipped), [1, 'hello']); } }); test('should return None if first is None', () => { const a = Optional.none; const b = Optional.some('hello'); const zipped = Optional.zip(a, b); assert.isTrue(Optional.isNone(zipped)); }); test('should return None if second is None', () => { const a = Optional.some(1); const b = Optional.none; const zipped = Optional.zip(a, b); assert.isTrue(Optional.isNone(zipped)); }); test('should return None if both are None', () => { const zipped = Optional.zip(Optional.none, Optional.none); assert.isTrue(Optional.isNone(zipped)); }); }); describe('fromNullable', () => { test('should convert non-null values to Some', () => { const helloOpt = Optional.fromNullable('hello'); if (Optional.isSome(helloOpt)) expect(Optional.unwrap(helloOpt)).toBe('hello'); const numOpt = Optional.fromNullable(42); if (Optional.isSome(numOpt)) expect(Optional.unwrap(numOpt)).toBe(42); const zeroOpt = Optional.fromNullable(0); if (Optional.isSome(zeroOpt)) expect(Optional.unwrap(zeroOpt)).toBe(0); const emptyOpt = Optional.fromNullable(''); if (Optional.isSome(emptyOpt)) expect(Optional.unwrap(emptyOpt)).toBe(''); const falseOpt = Optional.fromNullable(false); if (Optional.isSome(falseOpt)) assert.isFalse(Optional.unwrap(falseOpt)); }); test('should convert null to None', () => { assert.isTrue(Optional.isNone(Optional.fromNullable(null))); }); test('should convert undefined to None', () => { assert.isTrue(Optional.isNone(Optional.fromNullable(undefined))); }); test('should work with union types', () => { const value: string | null = 'test'; expectTypeOf(Optional.fromNullable(value)).toExtend<Optional<string>>(); }); }); describe('toNullable', () => { test('should convert Some to its value', () => { expect(Optional.toNullable(Optional.some(42))).toBe(42); expect(Optional.toNullable(Optional.some('hello'))).toBe('hello'); expect(Optional.toNullable(Optional.some(null))).toBeNull(); }); test('should convert None to null', () => { // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression expect(Optional.toNullable(Optional.none)).toBeUndefined(); }); test('should have correct return type', () => { const some = Optional.some(42); expectTypeOf(Optional.toNullable(some)).toExtend<number | undefined>(); }); }); describe('edge cases', () => { test('should handle undefined as a Some value', () => { const someUndefined = Optional.some(undefined); assert.isTrue(Optional.isSome(someUndefined)); // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression expect(Optional.unwrap(someUndefined)).toBeUndefined(); // This is different from None expect(someUndefined).not.toBe(Optional.none); }); test('should handle null as a Some value', () => { const someNull = Optional.some(null); assert.isTrue(Optional.isSome(someNull)); expect(Optional.unwrap(someNull)).toBeNull(); }); test('should handle nested Optionals', () => { const nested = Optional.some(Optional.some(42)); assert.isTrue(Optional.isSome(nested)); const inner = Optional.unwrap(nested); assert.isTrue(Optional.isOptional(inner)); expect(Optional.unwrap(inner)).toBe(42); }); }); });