/* eslint-disable unicorn/no-negated-comparison */ import { Arr, expectType, isBigint, Result } from 'ts-data-forge'; import { type BoolAnd, type BoolNot } from 'ts-type-forge'; import { refine } from '../other-types/index.mjs'; import { type Type } from '../type.mjs'; import { createPrimitiveType, type NumericConstraintViolation, } from '../utils/index.mjs'; type NumberType = bigint; export function bigint(defaultValue?: NumberType): Type; export function bigint( defaultValue: N & DefaultValueType, constraints: C, ): Type; export function bigint( defaultValue: NumberType = 0n, constraints?: C, ): Type { const baseType = createPrimitiveType({ typeName: 'bigint', defaultValue, is: isBigint, }); if ( constraints === undefined || Arr.isFixedLengthTuple(Object.keys(constraints), 0) ) { return baseType; } const constraintsPredicate = createConstraintsPredicate(constraints); const defaultValueConstraintsCheck = constraintsPredicate(defaultValue); if (Result.isErr(defaultValueConstraintsCheck)) { throw new Error( defaultValueErrorMessage( defaultValue, defaultValueConstraintsCheck.value, ), ); } return refine({ baseType, defaultValue, is: (value): value is NumberType => Result.isOk(constraintsPredicate(value)), typeName: 'bigint', getConstraintDetails: (value) => { const result = constraintsPredicate(value); return Result.isErr(result) ? { kind: 'numeric-constraint', numericType: 'bigint', violation: result.value, } : undefined; }, }); } type Constraints = Partial< Readonly<{ nonZero: true; negative: true; nonNegative: true; positive: true; nonPositive: true; gt: NumberType; gte: NumberType; min: NumberType; lt: NumberType; lte: NumberType; max: NumberType; multipleOf: NumberType; step: NumberType; }> >; type DefaultValueType< N extends NumberType, C extends Constraints, > = DefaultValueWhenNonZeroIsOn & DefaultValueWhenNegativeIsOn & DefaultValueWhenNonNegativeIsOn & DefaultValueWhenPositiveIsOn & DefaultValueWhenNonPositiveIsOn; type DefaultValueWhenNonZeroIsOn = C extends Readonly<{ nonZero: true }> ? NonZeroNumber : NumberType; type DefaultValueWhenNegativeIsOn = C extends Readonly<{ negative: true }> ? NegativeNumber : NumberType; type DefaultValueWhenNonNegativeIsOn< N extends NumberType, C extends Constraints, > = C extends Readonly<{ nonNegative: true }> ? NonNegativeNumber : NumberType; type DefaultValueWhenPositiveIsOn = C extends Readonly<{ positive: true }> ? PositiveNumber : NumberType; type DefaultValueWhenNonPositiveIsOn< N extends NumberType, C extends Constraints, > = C extends Readonly<{ nonPositive: true }> ? NonPositiveNumber : NumberType; type NonZeroNumber = NumberType extends N ? NumberType : IsZero extends true ? never : N; type NegativeNumber = NumberType extends N ? NumberType : IsNegative extends true ? N : never; type NonNegativeNumber = NumberType extends N ? NumberType : IsNonNegative extends true ? N : never; type PositiveNumber = NumberType extends N ? NumberType : IsPositive extends true ? N : never; type NonPositiveNumber = NumberType extends N ? NumberType : IsNonPositive extends true ? N : never; type IsZero = `${N}` extends '0' ? true : false; type IsNonZero = IsZero extends true ? false : true; type IsNegative = `${N}` extends `-${string}` ? true : false; type IsNonNegative = IsNegative extends true ? false : true; type IsPositive = BoolAnd, IsNonNegative>; type IsNonPositive = BoolNot>; const createConstraintsPredicate = (constraints: Constraints) => (value: NumberType): Result => { const { nonZero, negative, nonNegative, positive, nonPositive, gt, gte, min, lt, lte, max, multipleOf, step, ..._rest } = constraints; expectType('='); if (nonZero === true && !(value !== 0n)) { return Result.err({ constraint: 'nonZero', value: true } as const); } if (negative === true && !(value < 0n)) { return Result.err({ constraint: 'negative', value: true } as const); } if (nonNegative === true && !(value >= 0n)) { return Result.err({ constraint: 'nonNegative', value: true } as const); } if (positive === true && !(value > 0n)) { return Result.err({ constraint: 'positive', value: true } as const); } if (nonPositive === true && !(value <= 0n)) { return Result.err({ constraint: 'nonPositive', value: true } as const); } if (gt !== undefined && !(value > gt)) { return Result.err({ constraint: 'gt', value: String(gt) } as const); } if (gte !== undefined && !(value >= gte)) { return Result.err({ constraint: 'gte', value: String(gte) } as const); } if (min !== undefined && !(value >= min)) { return Result.err({ constraint: 'min', value: String(min) } as const); } if (lt !== undefined && !(value < lt)) { return Result.err({ constraint: 'lt', value: String(lt) } as const); } if (lte !== undefined && !(value <= lte)) { return Result.err({ constraint: 'lte', value: String(lte) } as const); } if (max !== undefined && !(value <= max)) { return Result.err({ constraint: 'max', value: String(max) } as const); } // `value % 0n` throws a RangeError, so the zero divisor is handled // separately: it only admits zero. if (multipleOf !== undefined) { if (multipleOf === 0n) { if (value !== 0n) { return Result.err({ constraint: 'multipleOf', value: String(multipleOf), } as const); } } else if (value % multipleOf !== 0n) { return Result.err({ constraint: 'multipleOf', value: String(multipleOf), } as const); } } if (step !== undefined) { if (step === 0n) { if (value !== 0n) { return Result.err({ constraint: 'step', value: String(step), } as const); } } else if (value % step !== 0n) { return Result.err({ constraint: 'step', value: String(step) } as const); } } return Result.ok(true); }; const defaultValueErrorMessage = ( value: NumberType, violation: NumericConstraintViolation, ): string => `defaultValue [${value}] for bigint does not satisfy the constraint ${violation.constraint} = ${violation.value}` as const;