Open
@AnyhowStep

Description

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...