Nurbol Alpysbayev
Nurbol Alpysbayev

Reputation: 21851

How can I produce an incremented version of a numeric literal type in TypeScript?

Is it possible from number type T to get number type Y that has value of T+1.

type one = 1

type Increment<T extends number> = ???

type two = Increment<one> // 2

P.S. Currently, I have hardcoded interface of incremented values, but the problem is hardcoded and hence limited:

export type IncrementMap = {
    0: 1,
    1: 2,
    2: 3,

Upvotes: 10

Views: 7545

Answers (8)

Rabah Taib
Rabah Taib

Reputation: 13

This solution has no range limit and works well with negative values BUT:

  • It only works with integers
  • May slow your IDE due to its expensive calculation (That's why I used @ts-expect-error)

// Main type

type Increment<Num extends number> = _IsFloat<Num> extends true
  ? never
  : _ToStr<Num> extends infer NumStr
  ? NumStr extends `-${infer PosNum extends number}`
    ? // @ts-expect-error - Expensive Calculation
      _ToNum<`-${_Decrement<PosNum>}`>
    : NumStr extends `${infer PosNum extends number}`
    ? _Increment<PosNum>
    : never
  : never;

// Helpers

type _IsFloat<Num extends number> = _ToStr<Num> extends `${infer Left}.${infer Right}` ? true : false;

type _Increment<PosNum extends number, ARR extends any[] = []> = PosNum extends ARR["length"]
  ? [...ARR, any]["length"]
  : _Increment<PosNum, [...ARR, any]>;

type _Decrement<PosNum extends number, ARR extends any[] = []> = PosNum extends 0
  ? -1
  : PosNum extends ARR["length"]
  ? ARR extends [infer First, ...infer Rest]
    ? Rest["length"]
    : never
  : _Decrement<PosNum, [...ARR, any]>;

type _ToStr<Type extends string | number | boolean | null | undefined | bigint> = `${Type}`;

type _ToNum<Str extends string> = Str extends `${infer Num extends number}` ? Num : never;

// Examples

type E1 = Increment<15>; // 16
type E2 = Increment<-10>; // -9
type E3 = Increment<0.5>; // never


NOTES:

If you want to use _ToNum utility type in other scenarios, use this instead:


// Main type

type ToNum<Str extends string> = _RemLeadingZeros<Str> extends infer ValidStr
  ? ValidStr extends `${infer Start}.${infer End}`
    ? (
        Start extends ""
          ? End extends ""
            ? never
            : `0.${End}`
          : End extends ""
          ? Start
          : Start extends "-"
          ? `-0.${End}`
          : ValidStr
      ) extends infer InferredStr
      ? InferredStr extends `${infer Num extends number}`
        ? Num
        : never
      : never
    : ValidStr extends `${infer Num extends number}`
    ? Num
    : never
  : never;

// Helpers

type _RemLeadingZeros<
  Str extends string,
  COUNT extends any[] = []
> = Str extends `${infer First}${infer Rest}`
  ? First extends "0"
    ? Rest extends ""
      ? First
      : _RemLeadingZeros<Rest, [...COUNT, any]>
    : First extends "-"
    ? COUNT["length"] extends 0
      ? Str
      : never
    : Str
  : never;

// Examples

type X1 = _ToNum<"05">; // 5
type X2 = _ToNum<".5">; // 0.5
type X3 = _ToNum<"5.">; // 5
type X4 = _ToNum<"-.5">; // -0.5

This one has more validations and works well in these examples While the previous _ToNum will return number instead of an actual number.

You don't need this type in the increment type, it will only add more expensive calculation, use the simpler one instead

Upvotes: 0

wonderflame
wonderflame

Reputation: 7642

The following solution has no recursion limit of n > 999. The only limit is n.toString().length > 1000; This seems to be a limit of javascript's ability to identify a number. Once you pass that limit, the typescript will show the incremented type as a number.

type IncrementMap = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
type LastDigitMap = {
  10: 0;
  11: 1;
  12: 2;
  13: 3;
  14: 4;
  15: 5;
  16: 6;
  17: 7;
  18: 8;
};

type LastCharacter<T extends string> = T extends `${infer First}${infer Last}`
  ? Last extends ''
    ? First
    : LastCharacter<Last>
  : T;

export type _Increment<
  Number extends string,
  Carry extends 0 | 1 = 0,
  Result extends string = '',
> = Number extends ''
  ? Carry extends 0
    ? Result
    : `${Carry}${Result}`
  : LastCharacter<Number> extends `${infer LastDigit extends number}`
  ? IncrementMap[LastDigit] extends infer Incremented extends number
    ? Number extends `${infer Rest}${LastDigit}`
      ? Incremented extends keyof LastDigitMap
        ? _Increment<Rest, 1, `${LastDigitMap[Incremented]}${Result}`>
        : `${Rest}${Incremented}${Result}`
      : never
    : never
  : never;

type Increment<T extends number> = _Increment<
  `${T}`,
  1
> extends `${infer Result extends number}`
  ? Result
  : never;

The logic of the given solution is similar to summing up big numbers as a string. We start with the rightest character and add one.

The addition is done with the IncrementMap type, where the indexes are the numbers we are trying to increment, and the values are the actual incremented numbers we want to get.

Testing:

type Case1 = Increment<1>; // 2
type Case2 = Increment<9>; // 10
type Case3 = Increment<999>; // 1000
type Case4 = Increment<1899999999999999> // 1900000000000000;

Link to playground

Upvotes: 0

Oleg
Oleg

Reputation: 11

@abdurahmanus solution with limit of 1000 recursive calls, but in 1 helper type:

type Inc<N extends number, T extends any[] = []> = T["length"] extends N ? [...T, any]["length"] : Inc<N, [...T, any]>;

Upvotes: 0

Amin
Amin

Reputation: 101

continuation to @jcalz comment above the Decrement:

type _DecDigit = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8];
type Digit = _DecDigit[number];

type _Dec<T extends string> = T extends "10"
  ? 9
  : T extends `${infer F}${Digit}`
  ? T extends `${F}${infer L extends Digit}`
    ? `${L extends 0 ? _Dec<F> : F}${_DecDigit[L]}`
    : never
  : 1;

export type Decrement<T extends number> = T extends 0
  ? number
  : T extends 10
  ? 9
  : number extends T
  ? number
  : `${T}` extends `${string}${"." | "+" | "-" | "e"}${string}`
  ? number
  : _Dec<`${T}`> extends `${infer N extends number}`
  ? N
  : never;

Upvotes: 0

jcalz
jcalz

Reputation: 327614

TypeScript still doesn't natively support mathematical operations on numeric literal types. There is a longstanding open feature request for it at microsoft/TypeScript#26382.

But nowadays you could, if you must, use template literal types to increment numeric literal types by converting the numeric literal into a string literal; performing character-by-character decimal addition on that string literal, and converting the resulting string literal back to a numeric literal.

Frankly this is probably overkill for any reasonable use case, but it's possible and doesn't tax the compiler too much.

In what follows, we will restrict the inputs to numeric literals that represent non-negative whole numbers less than Number.MAX_SAFE_INTEGER. For any other input (e.g., number itself, negative numbers, fractions, very large numbers) the output will just be number. These could mostly be worked around if necessary by careful review of the rules for Number.toString(10) and for the JavaScript encoding of numbers as double-precision 64-bit binary format IEEE 754, but I'm not going to bother with those here.


This is the implementation:

type _IncDigit = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
type Digit = _IncDigit[number];

type _Inc<T extends string> =
  T extends `${infer F}${Digit}` ? T extends `${F}${infer L extends Digit}` ?
  `${L extends 9 ? _Inc<F> : F}${_IncDigit[L]}` :
  never : 1

type Increment<T extends number> =
  number extends T ? number :
  `${T}` extends `${string}${"." | "+" | "-" | "e"}${string}` ? number :
  _Inc<`${T}`> extends `${infer N extends number}` ? N : never

The _IncDigit type is a utility tuple which encodes how to increment a single digit without worrying about carry; so _IncDigit[0] is 1, and _IncDigit[5] is 6, all the way up to _IncDigit[9] being 0.

Then _Inc<T> is the basic increment operation which assumes that T is a string representation of a valid numeric input. If T has at least one character it splits it into the last digit L and the stuff before it F. L can be incremented with _IncDigit, so IncDigit[L] is the last digit of the result. If L isn't 9 then we can just prepend F as-is; otherwise we also have to increment F, so we recurse to determine _Inc<F>.

Finally, Increment<T> takes care of turning strings to numbers and numbers to strings, as well as validating the input.

Let's test it out:

type Inc0 = Increment<0> // 1
type Inc5 = Increment<5> // 6
type Inc9 = Increment<9> // 10
type Inc8675309 = Increment<8675309>; // 8675310

type IncFrac = Increment<2.5> // number
type IncNeg = Increment<-2> // number
type IncTinyFrac = Increment<1.0e-4>; // number
type IncHuge = Increment<9.9e99> // number    
type IncNumber = Increment<number>; // number

type Inc3299999999999999 = Increment<3299999999999999>
// type Inc3299999999999999 = 3300000000000000

type CloseToMax = Increment<8999999999999999>
// type CloseToMax = 9000000000000000

type MaxSafeInteger = Increment<9007199254740991>
// type MaxSafeInteger = 9007199254740992

type TooBig = Increment<9007199254740992>
// type TooBig = number

Looks good.


Again, though, I don't know that such an approach is actually warranted by any normal use case. If you're doing this for fun, great. But if you think you find yourself wanting to do this for some production code base somewhere, consider carefully whether to what extent you actually need to re-implement mathematical operations in the type system. It's quite likely that a restricted approach such as hardcoding the results of incrementing all numbers from 0 to 100 or something, will also meet your needs.

Playground link to code

Upvotes: 7

jcalz
jcalz

Reputation: 327614

This answer is obsolete since recursive conditional types were introduced in TS4.1. And since TS4.8 introduced support for converting string literal types to number literal types. See this answer instead for a solution involving 🤢 base-10 arithmetic on string representations of numbers.


I would just hardcode it like this:

type Increment<N extends number> = [
  1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
  21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,
  38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54, // as far as you need
  ...number[] // bail out with number
][N]

type Zero = 0
type One = Increment<Zero> // 1
type Two = Increment<One>  // 2

type WhoKnows = Increment<12345>; // number

As I said in the other comments, there's currently no great support for this kind of naturally recursive type. I would love it if it were supported, but it's not there. In practice I've found that if something can handle tuples up to length 20 or so it's good enough, but your experience may differ.

Anyway, if anyone does come up with a solution here that isn't hardcoded but also works and performs well for arbitrary numbers (where Increment<123456789> will evaluate to 123456790) I'd be interested to see it. Maybe one day in the future it will be part of the language.

Upvotes: 6

abdurahmanus
abdurahmanus

Reputation: 61

A slightly different solution. But with the same limitations of 1000 recursive calls

type Arr<N extends number, T extends any[] = []> = T['length'] extends N ? T : Arr<N, [...T, any]>

type Inc<N extends number> = [...Arr<N>, any]['length']

type I20 = Inc<19>

Upvotes: 5

Holp
Holp

Reputation: 294

This solution is not hard coded but isn't useful due to TypeScript's recursion limits. When I've tested it, it does not handle Number2Nat greater than 45.

type SomeNat = [...unknown[]];
type Zero = [];
type Succ<N extends SomeNat> = [...N, unknown];
type Nat2Number<N extends SomeNat> = N["length"];
type Dec<N extends SomeNat> = N extends [unknown, ...infer T] ? T : never;
type Add<N extends SomeNat, M extends SomeNat> = [...N, ...M];
type Sub<N extends SomeNat, M extends SomeNat> = M extends Zero ? N : Sub<Dec<N>, Dec<M>>;
type Number2Nat<I extends number, N extends SomeNat = Zero> = I extends Nat2Number<N> ? N : Number2Nat<I, Succ<N>>;

Upvotes: 2

Related Questions