/* eslint-disable unicorn/no-negated-comparison */ import { Arr, expectType, Int, isNumber, Result } from 'ts-data-forge'; import { type BoolAnd, type BoolNot, type Brand, type FiniteNumber, type IsNever, type NegativeInt, type NonPositiveInt, type NonZeroFiniteNumber, type NonZeroInt, type PositiveInt, type SafeInt, type SafeUint, type Uint, } 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 = number; export function number(defaultValue?: NumberType): Type; export function number( defaultValue: N & DefaultValueType, constraints: C, ): Type>; export function number( defaultValue: NumberType = 0, constraints?: C, ): Type { const baseType = createPrimitiveType({ typeName: 'number', defaultValue, is: isNumber, }); 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: 'number', getConstraintDetails: (value) => { const result = constraintsPredicate(value); return Result.isErr(result) ? { kind: 'numeric-constraint', numericType: 'number', violation: result.value, } : undefined; }, }); } type Constraints = Partial< Readonly<{ finite: true; int: true; safeInteger: true; 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; }> >; /** * The set of constraints accepted by {@link number}. */ export type NumberTypeConstraints = Constraints; /** * The numeric-range subset of {@link NumberTypeConstraints}. These constraints * (`gt`, `gte`, `min`, `lt`, `lte`, `max`, `multipleOf`, `step`) add runtime * validation without changing the result brand, so the branded number types * under `predefined/brand/number` accept only this subset. To build a different * brand from a plain number, use {@link number} with the predicate constraints * instead, or the dedicated branded constructor for the target type. */ export type NumberRangeConstraints = Pick< NumberTypeConstraints, 'gt' | 'gte' | 'min' | 'lt' | 'lte' | 'max' | 'multipleOf' | 'step' >; type DefaultValueType< N extends NumberType, C extends Constraints, > = DefaultValueWhenNonZeroIsOn & DefaultValueWhenNegativeIsOn & DefaultValueWhenNonNegativeIsOn & DefaultValueWhenPositiveIsOn & DefaultValueWhenNonPositiveIsOn; type ConstraintsToResultType = ConstraintsToResultHelperType>; type ConstraintsToResultHelperType< R extends Readonly<{ brandKeys: string; brandFalseKeys: string }>, > = IsNever extends true ? NumberType : Brand; type ConstraintsToResultBrandKeys = | (C extends Readonly<{ int: true }> ? Readonly<{ brandKeys: 'Int' | 'Finite'; brandFalseKeys: 'NaNValue' }> : never) | (C extends Readonly<{ safeInteger: true }> ? Readonly<{ brandKeys: 'Finite' | 'Int' | 'SafeInt'; brandFalseKeys: 'NaNValue'; }> : never) | (C extends Readonly<{ finite: true }> ? Readonly<{ brandKeys: 'Finite'; brandFalseKeys: 'NaNValue' }> : never) | (C extends Readonly<{ nonZero: true }> ? Readonly<{ brandKeys: '!=0'; brandFalseKeys: 'NaNValue' }> : never) | (C extends Readonly<{ negative: true }> ? Readonly<{ brandKeys: '!=0' | '< 2^15' | '< 2^16' | '< 2^31' | '< 2^32' | '<=0'; brandFalseKeys: '>=0' | 'NaNValue'; }> : never) | (C extends Readonly<{ nonNegative: true }> ? Readonly<{ brandKeys: '>=0' | '> -2^16' | '> -2^32' | '>= -2^15' | '>= -2^31'; brandFalseKeys: 'NaNValue'; }> : never) | (C extends Readonly<{ positive: true }> ? Readonly<{ brandKeys: '>=0' | '!=0' | '> -2^16' | '> -2^32' | '>= -2^15' | '>= -2^31'; brandFalseKeys: '<=0' | 'NaNValue'; }> : never) | (C extends Readonly<{ nonPositive: true }> ? Readonly<{ brandKeys: '< 2^15' | '< 2^16' | '< 2^31' | '< 2^32' | '<=0'; brandFalseKeys: 'NaNValue'; }> : never); { type BrandKeysInt = ConstraintsToResultBrandKeys>; expectType('='); expectType('='); type BrandKeysPositiveInt = ConstraintsToResultBrandKeys< Readonly<{ int: true; positive: true }> >; expectType< BrandKeysPositiveInt['brandKeys'], | 'Int' | 'Finite' | '!=0' | '>=0' | '> -2^16' | '> -2^32' | '>= -2^15' | '>= -2^31' >('='); expectType('='); expectType>, FiniteNumber>( '=', ); expectType< ConstraintsToResultType>, NonZeroFiniteNumber >('='); expectType< ConstraintsToResultType>, NonZeroInt >('='); expectType< ConstraintsToResultType>, PositiveInt >('='); expectType< ConstraintsToResultType>, NegativeInt >('='); expectType< ConstraintsToResultType>, NonPositiveInt >('='); expectType< ConstraintsToResultType>, Uint >('='); expectType>, SafeInt>( '=', ); expectType< ConstraintsToResultType>, SafeUint >('='); } 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 { finite, int, safeInteger, nonZero, negative, nonNegative, positive, nonPositive, gt, gte, min, lt, lte, max, multipleOf, step, ..._rest } = constraints; expectType('='); if (finite === true && !Number.isFinite(value)) { return Result.err({ constraint: 'finite', value: true } as const); } if (int === true && !Int.is(value)) { return Result.err({ constraint: 'int', value: true } as const); } if (safeInteger === true && !Number.isSafeInteger(value)) { return Result.err({ constraint: 'safeInteger', value: true } as const); } if (nonZero === true && !(value !== 0)) { return Result.err({ constraint: 'nonZero', value: true } as const); } if (negative === true && !(value < 0)) { return Result.err({ constraint: 'negative', value: true } as const); } if (nonNegative === true && !(value >= 0)) { return Result.err({ constraint: 'nonNegative', value: true } as const); } if (positive === true && !(value > 0)) { return Result.err({ constraint: 'positive', value: true } as const); } if (nonPositive === true && !(value <= 0)) { 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 % 0` throws a RangeError, so the zero divisor is handled // separately: it only admits zero. if (multipleOf !== undefined) { if (multipleOf === 0) { if (value !== 0) { return Result.err({ constraint: 'multipleOf', value: String(multipleOf), } as const); } } else if (value % multipleOf !== 0) { return Result.err({ constraint: 'multipleOf', value: String(multipleOf), } as const); } } if (step !== undefined) { if (step === 0) { if (value !== 0) { return Result.err({ constraint: 'step', value: String(step), } as const); } } else if (value % step !== 0) { return Result.err({ constraint: 'step', value: String(step) } as const); } } return Result.ok(true); }; const defaultValueErrorMessage = ( value: NumberType, violation: NumericConstraintViolation, ): string => `defaultValue [${value}] for number does not satisfy the constraint ${violation.constraint} = ${violation.value}` as const;