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

Math with Number Literal Type #26382

Open
4 tasks done
AnyhowStep opened this issue Aug 11, 2018 · 25 comments · May be fixed by #48198
Open
4 tasks done

Math with Number Literal Type #26382

AnyhowStep opened this issue Aug 11, 2018 · 25 comments · May be fixed by #48198
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 11, 2018

Search Terms

number literal type, math

If there's already another such proposal, I apologize; I couldn't find it and have been asking on Gitter if such a proposal was already brought up every now and then.

Suggestion

We can have number literals as types,

const x : 32 = 34; //Error
const y : 5 = 5; //OK

If possible, math should be enabled with these number literals,

const x : 32 + 5 = 38; //Error
const y : 42 + 10 = 52; //OK
const z : 10 - 22 = -12; //OK

And comparisons,

const x : 32 >= 3 ? true : false = true; //OK
const y : 32 >= 3 ? true : false = false; //Error
//Along with >, <, <=, ==, != maybe?

And ways to convert between string literals and number literals,

//Not too sure about syntax
const w : (string)32 = "hello"; //Error
const x : (string)10 = "10"; //OK
const y : (number)"45" = 54; //Error
const z : (number)"67" = 67; //OK

Use Cases

One such use case (probably the most convincing one?) is using it to implement tuple operations.
Below, you'll find the types Add<>, Subtract<>, NumberToString<>, StringToNumber<>.

They have been implemented with... Copy-pasting code until the desired length.
Then, using those four types, the tuple operations are implemented.

While this works, having to copy-paste isn't ideal and shows there's something lacking in the language.
I've found that I've had to increase the number of copy-pastes every few days/weeks as I realize I'm working with larger and larger tuples over time.

The below implementation also ignores negative numbers for simplicity but supporting negative numbers would be good.

/*
function gen (max) {
	const base = [];
	const result = [];
	for (let i=0; i<max; ++i) {
		if (i == max-1) {
			base.push(`${i}: number;`);
        } else {
			base.push(`${i}: ${i+1};`);
        }
		if (i>=2) {
			result.push(`${i}: Add<Add<T, ${i-1}>, 1>;`);
        }
	}
	const a = base.join("\n        ");
	const b = result.join("\n    ");
	return `${a}\n    }[T];\n    ${b}`
}

gen(100)
*/

export type Add<T extends number, U extends number> = {
    [index: number]: number;
    0: T;
    1: {
        [index: number]: number;
        0: 1;
        1: 2;
        2: 3;
        3: 4;
        4: 5;
        5: 6;
        6: 7;
        7: 8;
        8: 9;
        9: 10;
        10: 11;
        11: 12;
        12: 13;
        13: 14;
        14: 15;
        15: 16;
        16: 17;
        17: 18;
        18: 19;
        19: 20;
        20: 21;
        21: 22;
        22: 23;
        23: 24;
        24: number;
    }[T];
    2: Add<Add<T, 1>, 1>;
    3: Add<Add<T, 2>, 1>;
    4: Add<Add<T, 3>, 1>;
    5: Add<Add<T, 4>, 1>;
    6: Add<Add<T, 5>, 1>;
    7: Add<Add<T, 6>, 1>;
    8: Add<Add<T, 7>, 1>;
    9: Add<Add<T, 8>, 1>;
    10: Add<Add<T, 9>, 1>;
    11: Add<Add<T, 10>, 1>;
    12: Add<Add<T, 11>, 1>;
    13: Add<Add<T, 12>, 1>;
    14: Add<Add<T, 13>, 1>;
    15: Add<Add<T, 14>, 1>;
    16: Add<Add<T, 15>, 1>;
    17: Add<Add<T, 16>, 1>;
    18: Add<Add<T, 17>, 1>;
    19: Add<Add<T, 18>, 1>;
    20: Add<Add<T, 19>, 1>;
    21: Add<Add<T, 20>, 1>;
    22: Add<Add<T, 21>, 1>;
    23: Add<Add<T, 22>, 1>;
    24: Add<Add<T, 23>, 1>;
}[U];


/*
function gen (max) {
	const base = [];
	const result = [];
	for (let i=1; i<=max; ++i) {
		base.push(`${i}: ${i-1};`);
		if (i>=2) {
			result.push(`${i}: Subtract<Subtract<T, ${i-1}>, 1>;`);
        }
	}
	const a = base.join("\n        ");
	const b = result.join("\n    ");
	return `${a}\n    }[T];\n    ${b}`
}

gen(100)
*/
export type Subtract<T extends number, U extends number> = {
    [index: number]: number;
    0: T;
    1: {
        [index: number]: number;
        0: number;
        1: 0;
        2: 1;
        3: 2;
        4: 3;
        5: 4;
        6: 5;
        7: 6;
        8: 7;
        9: 8;
        10: 9;
        11: 10;
        12: 11;
        13: 12;
        14: 13;
        15: 14;
        16: 15;
        17: 16;
        18: 17;
        19: 18;
        20: 19;
        21: 20;
        22: 21;
        23: 22;
        24: 23;
        25: 24;
    }[T];
    2: Subtract<Subtract<T, 1>, 1>;
    3: Subtract<Subtract<T, 2>, 1>;
    4: Subtract<Subtract<T, 3>, 1>;
    5: Subtract<Subtract<T, 4>, 1>;
    6: Subtract<Subtract<T, 5>, 1>;
    7: Subtract<Subtract<T, 6>, 1>;
    8: Subtract<Subtract<T, 7>, 1>;
    9: Subtract<Subtract<T, 8>, 1>;
    10: Subtract<Subtract<T, 9>, 1>;
    11: Subtract<Subtract<T, 10>, 1>;
    12: Subtract<Subtract<T, 11>, 1>;
    13: Subtract<Subtract<T, 12>, 1>;
    14: Subtract<Subtract<T, 13>, 1>;
    15: Subtract<Subtract<T, 14>, 1>;
    16: Subtract<Subtract<T, 15>, 1>;
    17: Subtract<Subtract<T, 16>, 1>;
    18: Subtract<Subtract<T, 17>, 1>;
    19: Subtract<Subtract<T, 18>, 1>;
    20: Subtract<Subtract<T, 19>, 1>;
    21: Subtract<Subtract<T, 20>, 1>;
    22: Subtract<Subtract<T, 21>, 1>;
    23: Subtract<Subtract<T, 22>, 1>;
    24: Subtract<Subtract<T, 23>, 1>;
    25: Subtract<Subtract<T, 24>, 1>;
}[U];


/*
function gen (max) {
	const base = [];
	for (let i=0; i<max; ++i) {
		base.push(`${i}: "${i}";`);
	}
	return base.join("\n    ");
}

gen(101)
*/
export type NumberToString<N extends number> = ({
    0: "0";
    1: "1";
    2: "2";
    3: "3";
    4: "4";
    5: "5";
    6: "6";
    7: "7";
    8: "8";
    9: "9";
    10: "10";
    11: "11";
    12: "12";
    13: "13";
    14: "14";
    15: "15";
    16: "16";
    17: "17";
    18: "18";
    19: "19";
    20: "20";
    21: "21";
    22: "22";
    23: "23";
    24: "24";
    25: "25";
    26: "26";
    27: "27";
    28: "28";
    29: "29";
    30: "30";
} & { [index : number] : never })[N];

/*
function gen (max) {
	const base = [];
	for (let i=0; i<max; ++i) {
		base.push(`"${i}": ${i};`);
	}
	return base.join("\n    ");
}

gen(101)
*/
export type StringToNumber<S extends string> = ({
    "0": 0;
    "1": 1;
    "2": 2;
    "3": 3;
    "4": 4;
    "5": 5;
    "6": 6;
    "7": 7;
    "8": 8;
    "9": 9;
    "10": 10;
    "11": 11;
    "12": 12;
    "13": 13;
    "14": 14;
    "15": 15;
    "16": 16;
    "17": 17;
    "18": 18;
    "19": 19;
    "20": 20;
    "21": 21;
    "22": 22;
    "23": 23;
    "24": 24;
    "25": 25;
    "26": 26;
    "27": 27;
    "28": 28;
    "29": 29;
    "30": 30;
} & { [index: string]: never })[S];

type LastIndex<ArrT extends any[]> = (
  Subtract<ArrT["length"], 1>
);
type IndicesOf<ArrT> = (
  Extract<
    Exclude<keyof ArrT, keyof any[]>,
    string
  >
);
type ElementsOf<ArrT> = (
  {
    [index in IndicesOf<ArrT>] : ArrT[index]
  }[IndicesOf<ArrT>]
);

type GtEq<X extends number, Y extends number> = (
  number extends X ?
  boolean :
  number extends Y ?
  boolean :
  number extends Subtract<X, Y> ?
  //Subtracted too much
  false :
  true
);
type KeepGtEq<X extends number, Y extends number> = (
  {
    [n in X]: (
      true extends GtEq<n, Y>?
        n : never
    )
  }[X]
)

type SliceImpl<ArrT extends any[], OffsetT extends number> = (
  {
    [index in Subtract<
      KeepGtEq<
        StringToNumber<IndicesOf<ArrT>>,
        OffsetT
      >,
      OffsetT
    >]: (
      ArrT[Extract<
        Add<index, OffsetT>,
        keyof ArrT
      >]
    )
  }
);
type Slice<ArrT extends any[], OffsetT extends number> = (
  SliceImpl<ArrT, OffsetT> &
  ElementsOf<SliceImpl<ArrT, OffsetT>>[] &
  { length : Subtract<ArrT["length"], OffsetT> }
);

declare const sliced0: Slice<["x", "y", "z"], 0>;
const sliced0Assignment: ["x", "y", "z"] = sliced0; //OK

declare const sliced1: Slice<["x", "y", "z"], 1>;
const sliced1Assignment: ["y", "z"] = sliced1; //OK

declare const sliced2: Slice<["x", "y", "z"], 2>;
const sliced2Assignment: ["z"] = sliced2; //OK

declare const sliced3: Slice<["x", "y", "z"], 3>;
const sliced3Assignment: [] = sliced3; //OK

//Pop Front
type PopFrontImpl<ArrT extends any[]> = (
  {
    [index in Exclude<
      IndicesOf<ArrT>,
      NumberToString<LastIndex<ArrT>>
    >]: (
      ArrT[Extract<
        Add<StringToNumber<index>, 1>,
        keyof ArrT
      >]
    )
  }
);
type PopFront<ArrT extends any[]> = (
  PopFrontImpl<ArrT> &
  ElementsOf<PopFrontImpl<ArrT>>[] &
  { length: Subtract<ArrT["length"], 1> }
);

//Kind of like Slice<["x", "y", "z"], 1>
declare const popped: PopFront<["x", "y", "z"]>;
const poppedAssignment: ["y", "z"] = popped; //OK

//Concat
type ConcatImpl<ArrT extends any[], ArrU extends any[]> = (
  {
    [index in IndicesOf<ArrT>] : ArrT[index]
  } &
  {
    [index in NumberToString<Add<
      StringToNumber<IndicesOf<ArrU>>,
      ArrT["length"]
    >>]: (
      ArrU[Subtract<index, ArrT["length"]>]
    )
  }
);
type Concat<ArrT extends any[], ArrU extends any[]> = (
  ConcatImpl<ArrT, ArrU> &
  ElementsOf<ConcatImpl<ArrT, ArrU>>[] &
  { length : Add<ArrT["length"], ArrU["length"]> }
);

declare const concat0: Concat<[], ["x", "y"]>;
const concat0Assignment: ["x", "y"] = concat0;

declare const concat1: Concat<[], ["x"]>;
const concat1Assignment: ["x"] = concat1;

declare const concat2: Concat<[], []>;
const concat2Assignment: [] = concat2;

declare const concat3: Concat<["a"], ["x"]>;
const concat3Assignment: ["a", "x"] = concat3;

declare const concat4: Concat<["a"], []>;
const concat4Assignment: ["a"] = concat4;

declare const concat5: Concat<["a", "b"], []>;
const concat5Assignment: ["a", "b"] = concat5;

declare const concat6: Concat<["a", "b"], ["x", "y"]>;
const concat6Assignment: ["a", "b", "x", "y"] = concat6;

type PushBackImpl<ArrT extends any[], ElementT> = (
  {
    [index in IndicesOf<ArrT>] : ArrT[index]
  } &
  {
    [index in NumberToString<ArrT["length"]>] : ElementT
  }
);

type PushBack<ArrT extends any[], ElementT> = (
  PushBackImpl<ArrT, ElementT> &
  ElementsOf<PushBackImpl<ArrT, ElementT>>[] &
  { length : Add<ArrT["length"], 1> }
);

declare const pushBack0: PushBack<[], true>;
const pushBack0Assignment: [true] = pushBack0;

declare const pushBack1: PushBack<[true], "a">;
const pushBack1Assignment: [true, "a"] = pushBack1;

declare const pushBack2: PushBack<[true, "a"], "c">;
const pushBack2Assignment: [true, "a", "c"] = pushBack2;

type IndexOf<ArrT extends any[], ElementT> = (
  {
    [index in IndicesOf<ArrT>]: (
      ElementT extends ArrT[index] ?
      (ArrT[index] extends ElementT ? index : never) :
      never
    );
  }[IndicesOf<ArrT>]
);

//Can use StringToNumber<> to get a number
declare const indexOf0: IndexOf<["a", "b", "c"], "a">; //"0"
declare const indexOf1: IndexOf<["a", "b", "c"], "b">; //"1"
declare const indexOf2: IndexOf<["a", "b", "c"], "c">; //"2"
declare const indexOf3: IndexOf<["a", "b", "c"], "d">; //Never
declare const indexOf4: IndexOf<["a", "b", "a"], "a">; //"0"|"2"
declare const indexOf5: IndexOf<["a", "b", "c"], "a" | "b">; //"0"|"1"

//Splice
//Pop Back
//Push Front
//And other tuple operations?

//Implementing Map<> is even worse, you basically have to copy-paste some boilerplate code
//for each kind of Map<> operation you want to implement because we
//can't have generic types as type parameters

Examples

Addition and subtraction should only allow integers

type example0 = 1 + 1; //2
type example1 = 1 + number; //number
type example2 = number + 1; //number
type example3 = number + number; //number
type example4 = 1.0 + 3.0; //4

type example5 = 1 - 1; //0
type example6 = 1 - number; //number
type example7 = number - 1; //number
type example8 = number - number; //number
type example9 = 1.0 - 3.0; //-2

If we did allow 5.1 - 3.2 as a type, we would get 1.8999999999999995 as a type.

type invalidSub = 5.1 - 3.2; //Error, 5.1 not allowed; must be integer; 3.2 not allowed; must be integer
type invalidAdd = 5.1 + 3.2; //Error, 5.1 not allowed; must be integer; 3.2 not allowed; must be integer

Maybe throw a compiler error on overflow with concrete numeric types substituted in,

//Number.MAX_SAFE_INTEGER + 1
type overflow = 9007199254740992 + 1; //Should throw compiler error; overflow

//Number.MIN_SAFE_INTEGER - 1000000
type overflow2 = -9007199254740991 - 1000000; //Should throw compiler error; overflow

type OverflowIfGreaterThanZero<N extends number> = (
    9007199254740992 + N
);
type okay0 = OverflowIfGreaterThanZero<0>; //Will be Number.MAX_SAFE_INTEGER, so no error
type okay1 = OverflowIfGreaterThanZero<number>; //No error because type is number
type err = OverflowIfGreaterThanZero<1>; //Overflow; error

Comparisons should work kind of like extends

type gt   = 3 >  2 ? "Yes" : "No"; //"Yes"
type gteq = 3 >= 2 ? "Yes" : "No"; //"Yes"
type lt   = 3 <  2 ? "Yes" : "No"; //"No"
type lteq = 3 <= 2 ? "Yes" : "No"; //"No"
type eq   = 3 == 3 ? "Yes" : "No"; //"Yes"
type neq  = 3 != 3 ? "Yes" : "No"; //"No"

If either operand is number, the result should distribute

type gt0 = number > 2 ? "Yes" : "No"; //"Yes"|"No"
type gt1 = 2 > number ? "Yes" : "No"; //"Yes"|"No"
type gt2 = number > number ? "Yes" : "No"; //"Yes"|"No"

Don't think floating-point comparison should be allowed.
Possible to have too many decimal places to represent accurately.

type precisionError = 3.141592653589793 < 3.141592653589793238 ?
    "Yes" : "No"; //Ends up being "No" even though it should be "Yes" because precision

Converting between string and number literals is mostly for working with tuple indices,

type example0 = (string)1; //"1"
type example1 = (string)1|2; //"1"|"2"
type example2 = (number)"1"|"2"; //1|2

Converting from integer string literals to number literals should be allowed, as long as within MIN_SAFE_INTEGER and MAX_SAFE_INTEGER,
but floating point should not be allowed, as it's possible that the string can be a floating point number that cannot be accurately represented.

For the same reason, converting floating point number literals to string literals shouldn't be allowed.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Since this suggestion is purely about the type system, it shouldn't change any run-time behaviour, or cause any JS code to be emitted.

I'm pretty sure I've overlooked a million things in this proposal...

@jcalz
Copy link
Contributor

jcalz commented Aug 11, 2018

In #26223 I wondered aloud if there was a canonical issue for dependent types for tuples. Still not sure, but this issue is obviously related to that one.

@ghost
Copy link

ghost commented Aug 11, 2018

Duplicate of #15645

@ghost ghost marked this as a duplicate of #15645 Aug 11, 2018
@ghost ghost added the Duplicate An existing issue was already created label Aug 11, 2018
@KiaraGrouwstra
Copy link
Contributor

@AnyhowStep I'd been interested in this as well, be it also mostly for tuple manipulation -- given tuple indices are restricted to natural numbers, I don't care as much about math on fractions.

I do see the TS team's considerations as valid though -- following a proposal like this would definitely add some complexity for the compiler. So features should be balanced with this complexity.

As a compromise, I actually quite like the approach of Flow's utility types, offering functionality without actually adding new syntax.

Now, your proposal to use these infix operators (+ - ? : > < >= <= == !=) obviously makes sense: they correspond to JavaScript equivalents, meaning zero learning curve to existing front-end engineers.

As much as I like it though, I think what sways considerations here toward compiler simplicity over low learning curve is, the target audience for type-level programming is very limited.

We definitely need more features, but even if they're still hard to use, that won't affect most front-end engineers -- we'll type the harder bits for them in the big libraries, at which point they can enjoy the superior type checks without knowing what changed under the hood.

I particularly like the Tail implementation! I'd tried similar things to yours here -- for inspiration, you might like the Add I had.

(As a disclaimer, don't expect to just compile my collection though. Recursion proved fragile, while TS was a moving target and testing types across versions remained a challenge. In particular, a breaking recursive type meant non-terminating compilation, which was quite hard to catch in unit tests.)

In #26223 I wondered aloud if there was a canonical issue for dependent types for tuples. Still not sure, but this issue is obviously related to that one.

@jcalz given [...T] should provide first-class support for most tuple manipulation, I'd say #5453 or #24897, though if the Concat above works, we might be just about done with tuples.
Unless you're interested in the recursion component, in which case mostly #14833, though the public stance seems potentially less favorable:

Should we close holes that allow people to write trivial turing machines in the type system?

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 9, 2020

Given the recent changes with the type system (mostly lifting conditional recursion and the addition of template string types), has there been any further consideration of this issue?

Though one of the concerns with this proposal was the possible union type explosion, such also applies to the recently added template string types, so it appears to not be an obstacle to this feature being implemented. Additionally, regarding the concern of the infix operators adding compiler complexity, the alternative of utility types can now be implemented, due to the addition of the intrinsic keyword in #40580.

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 9, 2020

Some additional thoughts on this proposal:

  • I think that some of the special cases in this proposal should be dropped, and rather use straight JavaScript arithmetic for all operations; the restrictions seem arbitrary, and it seems to make more sense that if .1 + .2 === .30000000000000004 in JavaScript, the same should apply to TypeScript type arithmetic.

  • A ParseInt, ParseFloat, ParseNumber, or similar would be useful, especially given the number to string conversion already present with template string types.

  • A Math namespace in lib.d.ts (or similar) would also be useful, and would reflect the built in Math functions in JS (e.g. Math.Sin<Math.PI * 2>). Excluding Math.random, I think all of the rest would be simple to implement as intrinsic utility types, and make this feature much more powerful.

  • A numeric range type would be useful, and could replace the special 1 < 2 ? A : B type proposed in this issue with 1 extends LessThan<2> ? A : B (with LessThan<2> being a placeholder for whatever syntax was adopted), and allow things such as types for positive numbers only.

  • int could be similarly useful (GreaterThanEq<0> & int for valid indexes, for example), and would be a decent result for Math.Floor<number>

One argument for not including some of all of these features is that they may increase type checking time when used, but people who need them will implement these or similar on their own, which will be much less efficient than a few extra utility types that many of which simply map to JS built-ins under the hood.

@Kingwl
Copy link
Contributor

Kingwl commented Oct 21, 2020

It's very helpful to me!

but people who need them will implement these or similar on their own, which will be much less efficient than a few extra utility types that many of which simply map to JS built-ins under the hood.

Cannot agree more.

@unional
Copy link
Contributor

unional commented Oct 21, 2020

Definitely see this is needed.

Agree with the points made by @tjjfvi .

As TypeScript gain more and more attraction and becoming more mature,
the frequency, and number of people desire to express complex types increases.
We need a more completed functional programming language at the type level to do all these. 🌷

@AnyhowStep
Copy link
Contributor Author

When I made this issue long ago, my only concern was math for integers and I think bigint wasn't really a thing yet.

And I believe there was resistance to having type level math because of floating point wonkiness. So, the proposal was trying to avoid that wonkiness.

Now that I look at it again, the wonkiness isn't so bad and if we need integer math, the bigint counterpart should be implemented. And maybe helpers to convert between bigint and number and string types

@GuiltyDolphin
Copy link

Support for natural numbers and corresponding operations would be extremely useful! Plenty of examples of how this can let you do some really neat stuff can be found at https://github.com/granule-project/granule.

@vitoke
Copy link

vitoke commented Aug 21, 2021

I agree it would be cool and useful, but can imagine it doesn't have top priority.

In the meantime, I've managed to get natural number calculation working up to 9999 in @rimbu/typical. Of course with a lot of workarounds but it works pretty well.

You can see it in action in this CodeSandbox

@roobscoob
Copy link

Bumping this further with #48198

@AverageHelper
Copy link

One example I'm facing today might be helped with this feature:

const units = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte", "petabyte"] as const;
const i: number = /* some math to select an index */;

// Current behavior (typescript 4.7.3)
const last: string = units[i] ?? units[units.length - 1]; // ERROR: Type 'string | undefined' is not assignable to type 'string'.

VSCode tells me that TypeScript sees units.length above as the constant 6. I would think that TypeScript could (should?) infer that 6 - 1 is always 5, and therefore infer that typeof units[units.length - 1] is typeof units[5], which is always "petabyte", and never undefined.

// Expected behavior
const last: string = units[i] ?? units[units.length - 1]; // OK: Type 'typeof units[number] | "petabyte"' is definitely assignable to type 'string'

I can work around this using the ! non-null assertion, but that's spoopy.

@JesseRussell411
Copy link

JesseRussell411 commented Nov 26, 2022

I definitely think this would be a good addition. If for no other reason than the fact that you can already do this in some really questionable ways using tuples and recursion.

@AntonPieper
Copy link

I also want this. I want to validate hex strings, which I could do if there were an easy way to do (integer) division in typescript's types.

@hazae41
Copy link

hazae41 commented May 10, 2023

I think I made something quite close, it can type check numbers fast, up to 2**16 (=65_536), but the compiler will sometimes yell at you, especially when both the left-hand and right-hand sides are generic

type X = Add<32_000, 32_123> // 64_123

type Y = Subtract<X, 64_000> // 123

type P = IsGreater<1_001, 1_000> // true
const x: 64_123 = add(32_000, 32_123)

const y: 123 = subtract(x, 64_000)

const p: true = greater(1_001, 1_000)

https://github.com/hazae41/integers

@JesseRussell411
Copy link

JesseRussell411 commented May 11, 2023

Can I point out that there's a case in the standard library where this feature is needed. Array's flat method takes a depth parameter which requires the ability to decrement a number literal in order to calculate the output type. Currently a tuple type is used with each element equal to one less than the index. I think it has 20 - 30 elements so a depth of, say, 40 would break it.

Not terribly realistic I know, but theoretically a problem at least.
If nothing else, just having depth - 1 would probably be cleaner.

@dest1n1s
Copy link

dest1n1s commented Sep 7, 2023

A potential use case of this feature is to create tensors with determined size in deep learning libraries. Currently most deep learning framework are written in dynamically typed languages such as python, and often we'll find a misalignment of tensor shape halfway through the running. With this feature it'll be possible to write definitely-sized tensor in TypeScript.

@unional
Copy link
Contributor

unional commented Sep 7, 2023

Just to share again, https://github.com/unional/type-plus has support some math functions.

And it supports int/float/bigint/negative

@hazae41
Copy link

hazae41 commented Sep 7, 2023

Just to share again, https://github.com/unional/type-plus has support some math functions.

And it supports int/float/bigint/negative

Are the performances great with large numbers and generics?

@hazae41
Copy link

hazae41 commented Sep 7, 2023

A potential use case of this feature is to create tensors with determined size in deep learning libraries. Currently most deep leraning framework are written in dynamically typed languages such as python, and often we'll find a misaligned of tensor shape halfway through the running. With this feature it'll be possible to write definitely-sized tensor in TypeScript.

I don't know about machine learning, but can you use this trick?

type Tensor<T, N extends number> = ArrayLike<T> & { length: N }

to create tensors with determined size e.g. Tensor<number, 128>

@unional
Copy link
Contributor

unional commented Sep 7, 2023

Are the performances great with large numbers and generics?

I don't think there are any significant performance issue. The calculations are finite and not consuming unbound memory.

@dest1n1s
Copy link

dest1n1s commented Sep 7, 2023

Just to share again, https://github.com/unional/type-plus has support some math functions.

And it supports int/float/bigint/negative

Thanks for your great contribution on type calculation! I pretty appreciate the idea of transforming number literals into digit arrays and calculate. It's the most efficient way for implementing multiplication I have seen. However, it seems lacking support of division calculator (maybe precision issues concern?). Furthermore, I think it more elegant implementing these basical arithmetics with a language level support, rather than resorting to type gymnastics.

I don't know about machine learning, but can you use this trick?

type Tensor<T, N extends number> = ArrayLike<T> & { length: N }

to create tensors with determined size e.g. Tensor<number, 128>

Certainly yes. But quite a number of tensor functions require support of some arithmetical calculations in its signature. A simple example is flatten, which literally flattens a tensor. The size of the output tensor of flatten should be a multiplication of each dimension of the input tensor.

@unional
Copy link
Contributor

unional commented Sep 7, 2023

Furthermore, I think it more elegant implementing these basical arithmetics with a language level support, rather than resorting to type gymnastics.

Definitely agree.

it seems lacking support of division calculator

Yes, precision is the main concern. The other one is complexity. 😛

@JesseRussell411
Copy link

It doesn't have to support a - b syntax. Just some intrinsic types like Add<A, B> and Subtract<A, B> would be good enough for me.

@DonnyVerduijn
Copy link

DonnyVerduijn commented Apr 24, 2024

Just having intrinsic types for arithmetic operations could potentially allow improved performance on a variety of usecases. Although i believe we shouldn't also have intrinsics to parse strings, here is an example of RGB and HEX validation, as @AntonPieper suggested. It's a great example of a common usecase if you ask me.

import { Call, N } from 'hotscript';
import { IsNumericLiteral } from 'type-fest';
// RGB color validation

type Int8<T extends number> =
  IsNumericLiteral<T> extends true
    ? Call<N.GreaterThanOrEqual<T, 0>> extends true
      ? Call<N.LessThanOrEqual<T, 255>> extends true
        ? true
        : false
      : false
    : false;

type CharToDigit = {
  '0': 0;
  '1': 1;
  '2': 2;
  '3': 3;
  '4': 4;
  '5': 5;
  '6': 6;
  '7': 7;
  '8': 8;
  '9': 9;
};

type Whitespace = ' ' | '\t' | '\n' | '\r';

// no negative, whitespace inbetween, decimal point or +3 digits
// removes whitespace at begin/end and reverses the string
type CleanAndReverse<
  S extends string,
  Acc extends string = '',
  Count extends number = 0,
> = S extends `${infer First}${infer Rest}`
  ? First extends Digit
    ? Count extends 3
      ? never // too many digits
      : CleanAndReverse<Rest, `${First}${Acc}`, Call<N.Add<Count, 1>>>
    : First extends Whitespace
      ? Count extends 0 | 3 // whitespace only after 0 or 3 digits
        ? CleanAndReverse<Rest, Acc, Count>
        : never // whitespace in wrong position
      : never // invalid char (negative sign etc.)
  : Acc;

type ParseReversedDecimalStr<
  S extends string,
  Multiplier extends number = 1,
  Acc extends number = 0,
> = S extends `${infer Char}${infer Rest}`
  ? Char extends keyof CharToDigit
    ? ParseReversedDecimalStr<
        Rest,
        Call<N.Mul<Multiplier, 10>>,
        Call<N.Add<Acc, Call<N.Mul<Multiplier, CharToDigit[Char]>>>>
      >
    : never
  : Acc;

// provide reversed string to peano number parser
type ParseInt<S extends string> =
  CleanAndReverse<S> extends ''
    ? never // empty values are not allowed
    : ParseReversedDecimalStr<CleanAndReverse<S>>;

export type ValidateRGB<T> = T extends `rgb(${infer R},${infer G},${infer B})`
  ? Int8<ParseInt<R>> extends true
    ? Int8<ParseInt<G>> extends true
      ? Int8<ParseInt<B>> extends true
        ? T
        : never
      : never
    : never
  : never;

// Hex color validation
// eslint-disable-next-line prettier/prettier
type HexChar = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type Hex = (HexChar & string) | (Digit & string);

type StripHash<T> = T extends `#${infer S}` ? S : never;
type ParseHex<
  T,
  Acc extends string = '#',
  Index extends number = 0,
> = T extends `${infer Char}${infer Rest}`
  ? Char extends Hex
    ? Rest extends ''
      ? Index extends 2 | 5 | 7
        ? `${Acc}${Char}` // final char
        : never // wrong length
      : ParseHex<Rest, `${Acc}${Char}`, Call<N.Add<Index, 1>>>
    : never // wrong char
  : never;

export type ValidateHex<T extends string> = ParseHex<StripHash<T>>;

// returns T as literal type if valid, otherwise never
export type ValidateColor<T extends string> = ValidateHex<T> | ValidateRGB<T>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.