Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PositiveIntegerString type #869

Open
mohsen1 opened this issue Apr 23, 2024 · 4 comments
Open

PositiveIntegerString type #869

mohsen1 opened this issue Apr 23, 2024 · 4 comments

Comments

@mohsen1
Copy link

mohsen1 commented Apr 23, 2024

This is more of a question for advanced type experts. Would it be possible to make a type that only allows positive integers in a string? E.g. "123" and "82739283293237"

In my attempt to use template string types with many unions will hit a wall with

Expression produces a union type that is too complex to represent.(2590)

Also posted this question here
ts-essentials/ts-essentials#388

See my attempt here

My attempt for PositiveIntegerString

type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
// Problem: This pattern can not be repeated for too long...
type PositiveIntegerString = ${Digits}${Digits | ''}${Digits | ''}${Digits | ''};

function processPositiveInteger(input: PositiveIntegerString) {
console.log("Valid positive integer string:", input);
}

// Example Usage:
processPositiveInteger("3"); // OK
processPositiveInteger("323"); // OK
processPositiveInteger("1323"); // OK
processPositiveInteger("1322323233"); // Should be OK but is not
processPositiveInteger("-1"); // Error
processPositiveInteger("12.34"); // Error

@sindresorhus
Copy link
Owner

Are you asking for "positive" or "non-negative"? Because 0 is not a positive number.

@sindresorhus
Copy link
Owner

We have some internal types here that may be useful as inspiration:

  • /**
    Converts a numeric string to a number.
    @example
    ```
    type PositiveInt = StringToNumber<'1234'>;
    //=> 1234
    type NegativeInt = StringToNumber<'-1234'>;
    //=> -1234
    type PositiveFloat = StringToNumber<'1234.56'>;
    //=> 1234.56
    type NegativeFloat = StringToNumber<'-1234.56'>;
    //=> -1234.56
    type PositiveInfinity = StringToNumber<'Infinity'>;
    //=> Infinity
    type NegativeInfinity = StringToNumber<'-Infinity'>;
    //=> -Infinity
    ```
    @category String
    @category Numeric
    @category Template literal
    */
    export type StringToNumber<S extends string> = S extends `${infer N extends number}`
    ? N
    : S extends 'Infinity'
    ? PositiveInfinity
    : S extends '-Infinity'
    ? NegativeInfinity
    : never;
  • /**
    Returns a boolean for whether `A` represents a number greater than `B`, where `A` and `B` are both positive numeric characters.
    @example
    ```
    PositiveNumericCharacterGt<'5', '1'>;
    //=> true
    PositiveNumericCharacterGt<'1', '1'>;
    //=> false
    ```
    */
    type PositiveNumericCharacterGt<A extends string, B extends string> = NumericString extends `${infer HeadA}${A}${infer TailA}`
    ? NumericString extends `${infer HeadB}${B}${infer TailB}`
    ? HeadA extends `${HeadB}${infer _}${infer __}`
    ? true
    : false
    : never
    : never;

@mohsen1
Copy link
Author

mohsen1 commented Apr 23, 2024

Are you asking for "positive" or "non-negative"?

Concretely non-negative. Thank you for clarification and the hints in the source code.

I believe recursively inferring the first digit and ensuring it is on of 0..9 digits should work. Let me try this!

@AshGw
Copy link

AshGw commented Apr 27, 2024

@mohsen1 Yes it is 100% possible:

const a0: PositiveIntegerStringType<'0'> = 0; // works
const a1: PositiveIntegerStringType<'82739283293237'> = 82739283293237; // works
const a2: PositiveIntegerStringType<'82739.283293237'> = 82739.28329323; // never
const a3: PositiveIntegerStringType<'-82739.283293237'> = 82739.28329323; // never
const a4: PositiveIntegerStringType<'-1'> = -1; // never

When it works, no squiggly lines, when it's never you get the squiggly lines.

Here's the type definition:

type PositiveIntegerStringType<S extends string> = IfEquals<
  IsPositiveInteger<Integer<NumerifyString<S>>>,
  true,
  Integer<NumerifyString<S>>,
  never
>;

with

type Numeric = number | bigint; 

/**
 * Turn a given string literal to a numeric 
 * @example 
 * 
 * NumerifyString<'54'>; // 54
 * NumerifyString<'699620.000000001'>; // 699620.000000001
  * IsNegativeFloat<NumerifyString<'-699620.000000001'>>; // true 
 */
export type NumerifyString<S extends string> = S extends `${infer N extends
  Numeric}`
  ? N
  : never;

/**
 * Type representing an integer
 */
export type Integer<N extends Numeric> = IfExtends<
  IsInteger<N>,
  true,
  N,
  never
>;


/**
 * Check if a given numeric value is an integer
 * @returns
 * true if it is, else false
 */
export type IsInteger<N extends Numeric> = number extends N
  ? false | true
  : N extends N
    ? `${N}` extends `${string}.${string}`
      ? false
      : true
    : never;

/**
 * Is it a positive integer ?
 * @return
 * `true` if it is, else `false`
 */
export type IsPositiveInteger<F extends Numeric> = IsPositive<Integer<F>>;

/**
 * Checks if a given numeric value is in [0,+∞[
 * @returns
 * true if it is, otherwise false
 */
export type IsPositive<N extends Numeric> = N extends N
  ? Numeric extends N
    ? boolean
    : `${N}` extends `-${Numeric}`
      ? false
      : true
  : never;


/**
 * Conditional type that checks if type `T` is equal to type `P`.
 * If `T` is equal to `P`, the type resolves to `Do`, otherwise `Else`.
 * @example
 *  type Result1 = IfEquals<string, string, true, false>; // is true
 *  type Result2 = IfEquals<number, string, true, false>; // is false
 *  type Result3 = IfEquals<boolean, boolean, true, false>; // is true
 *
 *  type IsExactlyString<T> = IfEquals<T, string, true, false>;
 *  type IsExactlyNumber<T> = IfEquals<T, number, true, false>;
 *
 *  type TestString = IsExactlyString<string>; // is true
 *  type TestNumber = IsExactlyNumber<number>; // is false
 *  type TestBoolean = IsExactlyString<boolean>; // is false
 */
export type IfEquals<T, P, Do, Else> = Equals<T, P> extends true ? Do : Else;


/**
 * Conditional  type that checks if two types `X` and `Y` are exactly equal.
 * If `X` is equal to `Y`, the  type resolves to `true`; otherwise `false`.
 * @example
 *  type Result1 = Equals<string, string>; // is true
 *  type Result2 = Equals<number, string>; // is false
 *  type Result3 = Equals<boolean | string, string | boolean>; // is true
 */
export type Equals<X, Y> = (<T>() => T extends X ? true : false) extends <
  T,
>() => T extends Y ? true : false
  ? true
  : false;

I hope that helps.

EDIT: I added it here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants