question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Math with Number Literal Type

See original GitHub issue

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…

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:40
  • Comments:13 (8 by maintainers)

github_iconTop GitHub Comments

5reactions
AnyhowStepcommented, Oct 21, 2020

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

4reactions
JesseRussell411commented, 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Numeric Literals - ActionScript: The Definitive Guide [Book]
The number type supports three kinds of literals: integer literals, floating-point literals, and special numeric values. The first two literal categories ...
Read more >
What is a literal number? - Study.com
Literal numbers are the letters used in an algebraic expression to represent a number or an unknown quantity. Literal numbers, or simply called...
Read more >
Handbook - Literal Types - TypeScript
There are three sets of literal types available in TypeScript today: strings, numbers, and booleans; by using literal types you can allow an...
Read more >
Implementing Arithmetic Within TypeScript's Type System |
Our goal is to have some type like Add that can take two numeric literal types and determine their sum. So we would...
Read more >
algebra introduction to literal numbers - YouTube
Algebra deals with unknown quantities known as variables. Variables are also known as literal numbers. Use of literal numbers allows us to ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found