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