Skip to content

Proposal: int types #4639

Description

This proposal introduces four new number types: int, uint, int<N> and uint<N>, where N is any positive integer literal. An int is signed, it can contain negative integers, positive integers and zero. An uint is an unsigned integer, it can contain positive integers and zero. int<N> and uint<N> are integers limited to N bits:

  • An int<N> can contain integers in the range -Math.pow(2, N-1) ... Math.pow(2, N-1) - 1.
  • An uint<N> can contain integers in the range 0 ... Math.pow(2, N) - 1

This proposal doesn't use type information for emit. That means this doesn't break compilation using isolatedModules.

Note: since JavaScript uses 64 bit floating point numbers, not all integers can be used at runtime. Declaring a variable with type like uint<1000> doesn't mean it can actually store a number like Math.pow(2, 999). These types are here for completeness and they can be used in type widening as used in type inference of binary operators. Most languages only support integer types with 8, 16, 32 and 64 bits. Integers with other size are supported because they are used in the type inference algorithm.

Goals

  • No emit based on type information
  • Report issues (with overflow or implicit floating point to integer casts) at compile time

References

Ideas were borrowed from:

  • asm.js
  • LLJS
  • C

Overview

int and uint are both subtypes of number. These types are the easiest integer types. They are not limited to a certain amount of bits. int<N> and uint<N> are subtypes of int and uint. These types have a limited size.

In languages like C, the result type of an operator is usually the same as the input type. That means you can get strange behavior, and possibly bugs:

uint8_t a = 200;
int b = a + a; // = 144 = 400 % 256, not 400
float c = 1 / 2; // = 0, not 0.5

To mimic that behavior we would need to add type converters everywhere in the emitted code, and those heavily rely on type information. We don't want that, so instead we widen the return types. That means there is no runtime overhead. For example, adding two values of type uint<8> would result in uint<9>. To assign that to a uint<8>, you can use an conversion function, like uint<8>(x). That function converts x to an uint<8>.

This design means that operations like x++, x += 10 and x /= 2 are not allowed, as they are not safe. Instead you can use x = uint<8>(x + 1) and x = uint<8>(x / 2).

int and uint allow more operations. Since they are not defined with a limited size, x++ and x += 10 are allowed. x-- is only allowed on an int, as an uint might become negative. Below is an overview of which operators are allowed on which number types.

Since emit isn't based on type information, integers can be used in generics. They can also be used in unions.

Assignability

  • int and uint are assignable to number
  • uint is assignable to int
  • int<N> is assignable to int and number
  • uint<N> is assignable to int, uint and number
  • int<N> is assignable to int<M> iff N <= M
  • uint<N> is assignable to uint<M> iff N <= M
  • int<N> is not assignable to uint<M> for all N, M
  • uint<N> is assignable to int<M> iff N < M
  • Infinity, -Infinity and NaN are not assignable to any integer type.
  • Literals are assignable if they don't have a decimal dot or an exponent, and fit in the range of the target type.
  • Enums are assignable if all defined values are assignable to the target type

If a type is not assignable to some other type, you can use a normal cast (<int<8>> x or x as int<8>), which has no impact at runtime, or a cast function (int<8>(x)).

Cast function

A cast function takes a number and converts it to the target integer type.

Syntax:

int<N>(x);
uint<N>(x);
int(x); // Alias of int<32>(x);
uint(x); // Alias of uint<32>(x);

Note: even though an int doesn't have a fixed size, we use the 32 bit cast function as that's easy and fast JavaScript.

Semantics:

  • Let x be the argument of the cast, converted to a number.
  • Round x (towards zero, -2.9 -> -2)
  • Truncate x to the target int / uint type.

This gives the same behavior as type casts in languages like C. If the operand is not a number, TS should give a compile time error. Emit should succeed (unless --noEmitOnError is set), the operand should be converted to a number at runtime, using same semantics as +x. undefined and null should also be converted the same way.

Implemented in TypeScript:

function castToInt(n: int, x: number) {
    x = +x;
    const m = Math.pow(2, n - 1);
    if (x > 0) {
        x = Math.floor(x);
        while (x > m) x -= 2 * m;
    } else {
        x = Math.ceil(x);
        while (x < -m) x += 2 * m;
    }
    return x;
}
function castToUint(n: int, x: number) {
    x = +x;
    const m = Math.pow(2, n);
    if (x > 0) {
        x = Math.floor(x);
        while (x > m) x -= m;
    } else {
        x = Math.ceil(x);
        while (x < 0) x += m;
    }
    return x;
}

These functions are not always used in the generated code. When n <= 32, these functions are not needed.
The generated code:

If n === 32:

int<32>(x); // TS
x | 0; // JS

uint<32>(x); // TS
x >>> 0; // JS

If n < 32,

uint(x);
uint<n>(x);
(x | 0) & b; // where b = pow(2, n) - 1

int(x);
int<n>(x);
(x | 0) << a >> a;
// where a = 32 - n

// Examples:
int<8>(x);
(x | 0) << 24 >> 24

uint<8>(x);
(x | 0) & 255;

Question: can we make the emit of int<n>(x) better? The current isn't very nice and performs bad.
Solved it using http://blog.vjeux.com/2013/javascript/conversion-from-uint8-to-int8-x-24.html

If n > 32:

uint<n>(x);
__castToUint(n, x);

int<n>(x);
__castToInt(n, x);

__castToUint and __castToInt are the functions above, emitted as helper functions.

You can only use these cast functions in call expressions:

int(x); // Ok
let intFunction = int; // Error
[1].map(int); // Error

We cannot solve that with helper functions, as int wouldn't be equal to int if they don't come from the same file when using external modules.
Instead we should dissallow this.

Type inference

Introducing integer types can break existing code, like this:

let x = 1;
x = 1.5;

But we do want to type 1 as an int:

let y: int = 1; // Don't show error that `number` is not assignable to `int`

There are several options:

  1. If a type of a variable is infered from a numeric literal, always infer to number.
  2. If a type of a variable is infered from a numeric literal, always infer to int. This would break some code, but we could accept that since this gives the most predictable behavior.
  3. Only infer to an integer if at least one part of the expression (variable, function call, ...) is explicitly typed as an integer.

In option two we infer to int, as let a = 0 would otherwise infer to uint<1>, which would mean that the variable can only contain
0 and 1.

Examples of option 3:

let a: int = 1;
let b = a + 3; // int
let c = 1 + 1; // number

function returnInt(): int {
    return 1;
}
let d = returnInt() - 1; // int
let e = int(1) * 1; // int
let f = <int> 1 + 1; // int
let g = <number> a + 1; // number

A literal will be infered to the smallest integer type that can contain the number. Examples:

0 -> uint<1>
1 -> uint<1>
2 -> uint<2>
-1 -> int<1>
-2 -> int<2>

Operators:

Operators should always infer to the smallest integer type that can contain all possible values.

(int, int<N>, uint or uint<N>) + number -> number
int + any integer type -> int
uint + (uint or uint<N>) -> uint
int<N> + int<M> -> int<max(N, M) + 1>
int<N> + uint<M> -> int<max(N, M + 1) + 1>
uint<N> + uint<M> -> uint<max(N, M) + 1>

- is almost the same as +, with two exceptions:
uint - (uint or uint<N>) -> int
uint<N> - uint<M> -> int<max(N, M) + 1>

int * (uint, int) -> int
uint * uint -> uint
int<N> * int<M> -> int<N + M>
(int<N + M - 1> is not big enough, consider -2^(N-1) * -2^(M-1) = 2^(N + M - 2)

/ always return `number`

int % (int, uint, int<N>, uint<N>) -> int
uint % (int, uint, int<N>, uint<N>) -> uint
int<N> % (int, uint) -> int<N>
uint<N> % (int, uint) -> uint<N>
int<N> % int<M> -> int<min(N, M)>
int<N> % uint<M> -> int<min(N, M+1)>
uint<N> % int<M> -> uint<max(min(N, M - 1), 1)>
uint<N> % uint<M> -> uint<min(N, M)>

int & (int or int<N>) -> int<32>
int<N> & int<M> -> int<min(N, M, 32)>
uint<N> follows the rules of int<N+1>
(uint & uint !== uint, for example 4294967295 & 4294967295 === -1)

| and ^ have the same behavior as &

~int -> int
~uint -> int
~int<N> -> int<N>
~uint<N> -> int<N + 1>

<< always returns an int<32>

(number, uint) >> any -> uint<32>
int >> any -> int<32>
int<N> >> any -> int<min(N, 32)>
uint<N> >> any -> uint<min(N, 32)>

(number, uint or int) >> any -> uint<32>
int<N> >>> any -> uint<32> (Consider -1 (int<1>), -1 >>> 0 === 429467295 === max value of uint<32>
uint<N> >>> any -> uint<min(N - 1, 32)>

Certain assignment operators are not supported on integer types. In short:
Let Op be an operator. x Op= y is allowed iff x = x Op y is allowed.
That means that the following operators are not supported and usage will give an error:

int: x /= y

uint: x /= y, x--, --x

int<N>, uint<N>: x /= y, x++, ++x, x--, --x, x += y, x -= y, x *= y

int<N>
if N < 32, then: x <<= y
if N <= 32, then: x >>>= y

uint<N>
if N < 32, then: x <<= y, x >>>= y

Breaking changes

Type inference can change depending on how it will be implemented. Also changing existing definitions can break things:

let arr = [];
let length = arr.length;
length = 1.5;

Changing the definition of the length property to a uint would break this code, though I don't think this pattern will be used a lot.
Such problems can easily be fixed using a type annotation (in this case let length: number = arr.length;).

Questions

  1. Should integer types be namespaced under number? Thus, let x: int vs. let x: number.int
  2. Which syntax should be used for sized integers? int<8>/number.int<8> or int8/number.int8?
  3. Can we make the emit of int<n>(x) (n < 32) better? The current looks and performs bad. Solved using http://blog.vjeux.com/2013/javascript/conversion-from-uint8-to-int8-x-24.html
  4. How should type inference be handled? See the question above, in paragraph called 'Type inference'.
  5. Can undefined and null be assigned to an integer type? The conversion functions convert them to 0. Allowing undefined would mean that int + int could be NaN (if one operand is undefined), while NaN is not assignable to an int. I'd say that undefined and null shouldn't be assignable to an integer type, and that declaring a variable (also class property) with an integer type and without an initializer would be an error.

All feedback is welcome. If you're responding to one of these questions, please include the number of the question. If this proposal will be accepted, I can try to create a PR for this.

Activity

  1. rotemdan commented on Sep 5, 2015

    @rotemdan

    For the more common cases of uint8, uint16, uint32, int8, int16, int32 there are simple tricks/bit "twiddles" that can make the truncations much faster/simpler. I tried to make them fit in one statement (sacrificing a tiny bit of performance in some), to make them better candidates for inlining (either through the JS runtime or TypeScript itself).

    Unsigned:

    function toUint8(num: number): uint8 {
        return num & 255;
    }
    
    function toUint16(num: number): uint16 {
        return num & 65535;
    }
    
    function toUint32(num: number): uint32 {
        return (num | 0) >= 0 ? (num | 0) : (num | 0) + 4294967296;
    }

    Signed:

    function toInt8(num: number): int8 {
        return (num & 255) < 128 ? (num & 255) : (num & 255) - 256;
    }
    
    function toInt16(num: number): int16 {
        return (num & 65535) < 32768 ? (num & 65535) : (num & 65535) - 65536;
    }
    
    function toInt32(num: number): int32 {
        return num | 0;
    }

    These functions would truncate to the least significant 8, 16 or 32 integer bits of any floating point number. Numbers outside of the range [-2^52..2^52 - 1] would still work but would be naturally limited in resolution.

    I've ran some automated testing for these with millions of random values against the expected truncation behavior in typed arrays and they seem to give exactly similar results including when the number is NaN, -Infinity and +Infinity, null, undefined, or any other type, including a string or an object. anything | 0 and anything & num always resolve to 0 in these cases [with the special exception of boolean where true is automatically converted to 1 and false to 0].


    There is an alternative method to do this that may or may not be faster (depending on the runtime), but would require runtime support for typed arrays (only available in IE10+):

    Unsigned:

    uint8Array_dummy = new Uint8Array(1);
    uint16Array_dummy = new Uint16Array(1);
    uint32Array_dummy = new Uint32Array(1);
    
    function toUint8(num: number): uint8 {
        uint8Array_dummy[0] = num; 
        return uint8Array_dummy[0];
    }
    
    function toUint16(num: number): uint16 {
        uint16Array_dummy[0] = num; 
        return uint16Array_dummy[0];
    }
    
    function toUint32(num: number): uint32 {
        uint32Array_dummy[0] = num; 
        return uint32Array_dummy[0];
    }

    Signed:

    int8Array_dummy = new Int8Array(1);
    int16Array_dummy = new Int16Array(1);
    int32Array_dummy = new Int32Array(1);
    
    function toInt8(num: number): int8 {
        int8Array_dummy[0] = num; 
        return int8Array_dummy[0];
    }
    
    function toInt16(num: number): int16 {
        int16Array_dummy[0] = num; 
        return int16Array_dummy[0];
    }
    
    function toInt32(num: number): int32 {
        int32Array_dummy[0] = num; 
        return int32Array_dummy[0];
    }

    Anyway, this should be used as a reference to test the correctness of the truncations.

    [Edit: I retested the first version and it does seem to return 0for NaN, Infinity, undefined etc. as expected, the problems happened in previous versions of the functions]

  2. ivogabe commented on Sep 5, 2015

    @ivogabe
    ContributorAuthor

    I've changed the emit for int<n>(x) where n < 32. It now uses bit shifting as explained here. This is probably the best solution for question 3.

    Rotem Dan (@rotemdan) Those conversions exist for all integer types up to 32 bits. You can see them all in the section "Cast function". TS should inline these functions, and browsers can optimize these constructs. The castToInt and castToUint are functions that demonstrate the expected behavior, the alternatives (like x | 0) follow that behavior.

  3. rotemdan commented on Sep 5, 2015

    @rotemdan

    Ivo Gabe de Wolff (@ivogabe)

    Great find! so let's try to summarize the simplest, most efficient implementations we have so far for the most important cases:

    Unsigned:

    • uint8: num & 255
    • uint16: num & 65535
    • uint32: num >>> 0

    Signed:

    • int8: (num << 24) >> 24
    • int16: (num << 16) >> 16
    • int32: num | 0

    I ran them through the automated tests (1M random FP values up to about +/- 2^71) and they seem to work correctly.

    It seems reasonable to have the behavior standardized against assignments to typed arrays so these should all yield 0 for NaN, undefined, string etc. not sure about boolean (right now some of these will give 1 for true but that's a really minor detail).

    [Edit: I confirmed typedArray[0] = true would set the value to 1, so there is no need for a correction there.]

    Another minor detail: I'm not sure if adding | 0 is needed for to be recognized as integers by asm.js (I'm not very familiar with it), I assume the JS runtime could be "smart" enough to recognize them?

  4. ivogabe commented on Sep 5, 2015

    @ivogabe
    ContributorAuthor

    Rotem Dan (@rotemdan) Most JS engines can optimize | 0 very well, but other binary operators are less optimized. I haven't measured the difference though. NaN, +/-Infinity, undefined and null should be converted to 0, strings are first converted to a number (eg "120" -> 120, "1.5" -> 1.5 -> 1). For booleans, true is converted to 1 and false to 0. That behavior is consistent between the proposed casts and typed arrays.

  5. rotemdan commented on Sep 5, 2015

    @rotemdan

    You mentioned that operations like x++, x += 10 and x /= 2 are not allowed without a cast. I understand the desire for safety but I still think that having an automatic cast/truncation on assignment is still reasonable for practical purposes (and is actually a useful/desirable thing to have for some applications).

    I mean that:

    let num: uint8 = 255;
    num = num + 1;

    Would emit:

    var num = 255;
    num = num + 1;
    num &= 255;

    The automatic casts would only happen on assignments, "anonymously typed" intermediate results would not be effected:

    let num: uint8 = 255;
    num = (num * num) + (num * 2);

    Still emits only a single truncation:

    var num = 255;
    num = (num * num) + (num * 2);
    num &= 255;

    The reason I wanted to "optimize" and simplify the casts (including the logic for them) as much as possible was because I wanted to make them extremely cheap for these purposes (In terms of performance, I think the operations turned up cheap enough to be called very frequently, including in tight loops).

    Since typed arrays automatically truncate on assignment and don't error on overflows or even on type mismatches, it seems like a reasonable compromise to have a somewhat more permissive logic. Not having these automatic casts would make it difficult to work with these types in practice (in some cases an explicit cast would be needed at every single line).

    It is still possible to limit automatic casts/truncations to only work between integer types, though they would not be required to be compatible:

    let num: uint8 = 255;
    
    num = num * 23; // OK, truncated to least significant 8 bits on assignment. ("modulo 2^8").
    
    num = num * 23.23423425; // Error, incompatible numeric types
    num = <uint8> num * 23.23423425; // OK

    Edit: It is possible to limit this even further to only apply to anonymous/intermediate expressions. Assignments between named variables may still be strict:

    let num1: uint8 = 45;
    let num2: uint16 = 11533;
    
    num1 = num2; // Error: incompatible numeric types
    num1 = <uint8> num2; // OK

    It might be possible to strengthen it back with even more restrictions. I guess there's a balance here between safety and usability.

  6. ivogabe commented on Sep 5, 2015

    @ivogabe
    ContributorAuthor

    Rotem Dan (@rotemdan) The behavior you suggest would require emit based on type information. That means isolatedModules / transpile (compilation per file in stead of one compilation of the whole project) cannot be used with integers. The key feature of this proposal is that it does not use type information for emit. That way the feature fits better in TypeScript.

    If you want less restrictions you can use int or uint, which are not restricted to a specific size. You can write this:

    let x: uint = 0;
    x++;

    The int and uint types give less guarantees (the only guarantees are that a value is round and in case of an uint, not negative), but are easier to use. In most cases you can use these types.

  7. dead-claudia commented on Sep 5, 2015

    @dead-claudia

    Ivo Gabe de Wolff (@ivogabe) Very correct.

    I would think it would make more sense to just start out with four types:

    • int - A 32-bit signed integer. This would go in line with most of JavaScript's bitwise operators.
    • uint - A 32-bit unsigned integer. This would account for the following cases (and a few others on the Math object):
      • int >>> int -> uint
      • uint >>> (int | uint) -> uint
      • int + int -> uint
      • int - int -> uint
    • float - Math.fround(x) -> float
    • double - Everything else.

    This is very similar to the asm.js types, and are already heavily optimized (V8 and Spidermonkey both generate type-specific code already for non-asm.js code that rely on these types). Matter of fact, almost all the asm.js types can be represented by these types + the type system.

    asm.js TypeScript
    void void
    signed int
    unsigned uint
    int `int
    fixnum int & uint
    intish `int
    double double
    double? `double
    float float
    float? `float
    floatish `float
    extern any

    Note: asm.js requires intish to be coerced back into int or uint and floatish to be coerced back into double or float. TypeScript wouldn't require it, but people can still remain explicit when necessary.

    As for implementation, there is no realistic way to guarantee these types without relying on explicit coercions.

    int -> x | 0
    uint -> x >>> 0
    int & uint -> x & <int & uint> y // e.g. y = 0x7fffffff
    float -> Math.fround(x) or a Float32Array member
    double -> +x
    // int, uint, and float are each subtypes of double
    // double is a subtype of number
    
    function f(x: number): int {
      // Error!
      // return x
      return x | 0
    }

    Even asm.js has to use such coercions to keep the math correct. Unlike asm.js, though, you don't have to coerce everything, so you don't have to write expr(x) | 0 a bunch of times. You also don't have to coerce your arguments, as the argument types are checked statically.

  8. ivogabe commented on Sep 6, 2015

    @ivogabe
    ContributorAuthor

    Floats and doubles would indeed be a good addition. For those who don't see the advantage, I'd suggest reading this: https://blog.mozilla.org/javascript/2013/11/07/efficient-float32-arithmetic-in-javascript/. I would call these types float32 and float64, as that's more in line with the names for typed arrays and SIMD in JS.

    The reason I chose to introduce more integer types is type safety. int32 + int32 isn't always an int32, so we must either add a conversion around it (implicitly), which would require type information, or widen the type to an int33. I chose the latter. The user would need to add the conversions himself. If he doesn't want that, he can use the 'unsized' integer type int, which doesn't check the size.

    My proposal introduces syntactic sugar for those casts, as I'd rather write int(x) than x | 0 and uint<8>(x) than (x | 0) & 256. Of course this can be extended to floats: float32(x) -> Math.fround(x) and float64(x) -> +x.

    IMPinball int + int -> uint isn't always true, consider 1 + -3 === -2

  9. dead-claudia commented on Sep 6, 2015

    @dead-claudia

    Ivo Gabe de Wolff (@ivogabe) I forgot about that... Oops. ;)

    And I'm aware that type casts that end up in the emit would definitely be useful. But I'm taking into account the TypeScript compiler devs generally steer away from it.

    If you want to create a third party compiler (a la CoffeeScriptRedux) with that as a nonstandard extension, go for it!

  10. dead-claudia commented on Sep 6, 2015

    @dead-claudia

    Or even edit the current one to do that, that'd still be fine.

  11. dead-claudia commented on Sep 6, 2015

    @dead-claudia

    And as for the link, I read it while creating this post. (I optimized the polyfill shortly after.)

    One thing that will have to be addressed with yours is right shifts with integers smaller than 32-bit. That can easily bring garbage in. And that would require those types making it into the emit.

  12. ivogabe commented on Sep 6, 2015

    @ivogabe
    ContributorAuthor

    IMPinball At first I found it strange that they didn't want to use type info, but now I think they're right about that. Using isolatedModules compilation times can get decreased from a few seconds to less than a second. Introducing a feature that would break that would be a very bad idea. Someone of the team can explain this better probably.

    I don't really like the idea of forking, it's not hard to create a fork, maintenance is usually the problem.

    One thing that will have to be addressed with yours is right shifts with integers smaller than 32-bit. That can easily bring garbage in. And that would require those types making it into the emit.

    I'm not sure what you mean with this, can you clarify that? Right shift (x >> y) removes bits and is sign preserving, so it's guaranteed that abs(x >> y) <= abs(x). Left shift doesn't have such guarantee, it should always infer to int<32>.

  13. dead-claudia commented on Sep 7, 2015

    @dead-claudia

    Ivo Gabe de Wolff (@ivogabe)

    Where the shifts can lead to unexpected behavior is this:

    Let's assume only the rightmost 8 bits are used. JavaScript numbers are only 32-bit, but for the sake of brevity, let's assume they're 16-bit.

    Let's try a left shift:

              |-------| <-- The digits we care about
    0000 0110 1100 0111 <-- x
    0001 1011 0001 1100 <-- x << 2, okay if uint8
    
    0000 0110 1100 0111 <-- original x
    0000 0001 1011 0001 <-- x >> 2, naive
    0000 0000 0011 0001 <-- x >> 2, correct
    

    The example of the left shift introduces artifacts for signedness because then if you try to add, it's adding a positive. The example of the right shift is that you're literally introducing garbage into the program, like what you can get from allocating a raw memory pointer without zeroing the region out first.

    Now, let's assume only the leftmost 8 bits are used. That'll address the issue of sign propagation and addition, subtraction, etc., but what about left and right shifts?

    |-------| <-- The part we are using
    1101 1001 1100 0000 <-- x
    0110 0111 0000 0000 <-- x << 2, naive
    0110 0100 0000 0000 <-- x << 2, correct
    

    Now, there's a similar garbage problem with left shifts. Simply because the other half wasn't zeroed out first.

    Obviously, these are only 16-bit integers and JavaScript uses 32-bit for its bitwise operations, but the argument could easily be expanded to cover those.

    In order to fix this, you need those types to correct the emit. And I didn't get into addition, subtraction, etc., because that's already been made.


    As another nit, I don't like the generic syntax, because a) you'd have to special case the parser (numbers aren't valid identifiers for generics), b) they're a finite, limited set of types, not some sort of parameterized type, and c) it doesn't seem right with the rest of the language, as primitives are just a simple identifier word.

  14. 29 remaining items

  15. mihailik commented on Mar 20, 2017

    @mihailik
    Contributor

    Apparently, current TS already supports a subset of my proposal above:

    type byte = 0|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|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|126|127|128|129|130|131|132|133|134|135|136|137|138|139|140|141|142|143|144|145|146|147|148|149|150|151|152|153|154|155|156|157|158|159|160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175|176|177|178|179|180|181|182|183|184|185|186|187|188|189|190|191|192|193|194|195|196|197|198|199|200|201|202|203|204|205|206|207|208|209|210|211|212|213|214|215|216|217|218|219|220|221|222|223|224|225|226|227|228|229|230|231|232|233|234|235|236|237|238|239|240|241|242|243|244|245|246|247|248|249|250|251|252|253|254|255;
    
    type sbyte = -128|-127|-126|-125|-124|-123|-122|-121|-120|-119|-118|-117|-116|-115|-114|-113|-112|-111|-110|-109|-108|-107|-106|-105|-104|-103|-102|-101|-100|-99|-98|-97|-96|-95|-94|-93|-92|-91|-90|-89|-88|-87|-86|-85|-84|-83|-82|-81|-80|-79|-78|-77|-76|-75|-74|-73|-72|-71|-70|-69|-68|-67|-66|-65|-64|-63|-62|-61|-60|-59|-58|-57|-56|-55|-54|-53|-52|-51|-50|-49|-48|-47|-46|-45|-44|-43|-42|-41|-40|-39|-38|-37|-36|-35|-34|-33|-32|-31|-30|-29|-28|-27|-26|-25|-24|-23|-22|-21|-20|-19|-18|-17|-16|-15|-14|-13|-12|-11|-10|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|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|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|126|127;
    
    
    function takeByte(x: byte): byte {
        x++;
        return x + 1;
    //  ~~~~~~~~~~~~~   Type 'number' is not assignable to type 'byte'.
    }
    
    takeByte(0);
    takeByte(10);
    takeByte(888);
    //       ~~~   Argument of type '888' is not assignable to parameter of type 'byte'.
    
    
    function byte2sbyte(x: byte): sbyte {
      if (this) return x + 1;
    //          ~~~~~~~~~~~~~  Type 'number' is not assignable to type 'byte'.
      else return x;
    //     ~~~~~~~~~   Type 'byte' is not assignable to type 'sbyte'.
    //                   Type '128' is not assignable to type sbyte.
    }

    The missing pieces are:

    • Can't do it for the adult grown-up integers, need actual compiler support
    • Explicit coercions still require type assertions (i.e. myNumber & 0xFF don't produce byte) — unlike ASM.js which has coercions that ensure output is always integer (or float).
    • Looks like TSC has a bug (see first line of takeByte function above), increment operator shouldn't be allowed on literal union.
  16. trusktr commented on May 1, 2017

    @trusktr
    Contributor

    ints would be nice. For example, consider this class:

    class KeyFrameManager {
      currentFrame:number
      // the rest of class exposes API for switching to different frames of a key-framed animation. 
    }

    There's currently no way to enforce that frames should be whole numbers, which means if a developer introduces a calculation that produces a non-whole number there could be strange bugs.

    Let us enforce this. 👍 For example:

    class KeyFrameManager {
      currentFrame:int
      // the rest of class exposes API for switching to different frames of a key-framed animation. 
    }

    This is simply a matter of enforcing program correctness for me, I don't care that numbers are actually all the same in JavaScript.

  17. yahiko00 commented on Jun 22, 2017

    @yahiko00

    Here is a definition file for numeric types used in AssemblyScript, an experimental compiler from TypeScript to WebAssembly, written in TypeScript: https://github.com/dcodeIO/AssemblyScript/blob/master/assembly.d.ts

    This could be a good basis for the future numerical types in the official TypeScript compiler.
    Also, until this time, I plan to use this definition file for my projects.

  18. matthew-dean commented on Jun 22, 2017

    @matthew-dean
  19. yahiko00 commented on Jun 22, 2017

    @yahiko00

    That's right. I've also had a look at this project but it seems a little but more "experimental" than AssemblyScript, and its definition file is much less complete.

  20. dead-claudia commented on Jun 25, 2017

    @dead-claudia

    Yahiko Uzumaki (@yahiko00) The main difference is that TurboScript aims to be a bit more fully featured and is closer to a TS derivative rather than AssemblyScript is (which is more or less just a retargeted TS).

  21. yahiko00 commented on Jun 25, 2017

    @yahiko00

    @isiahmeadows You are probably right since I am discovering these repos. Although, I like the idea of AssemblyScript to be a subset of TypeScript. Maybe I am wrong, but I think, in the future, this could be easier to merge AssemblyScript into the official TypeScript compiler.

  22. yahiko00 commented on Jun 29, 2017

    @yahiko00

    Here is another project, wasm-util, from TypeScript to WebAssembly: https://github.com/rsms/wasm-util

  23. tarcieri commented on Jul 11, 2017

    @tarcieri

    In case you missed #15096 I think TC39 BigInt is the best way forward to get true integers in TypeScript.

    This proposal would enable an arbitrary precision integer type and could also hint to JavaScript VMs when they could use e.g. native 64-bit integers, enabling things like an int64 and uint64 type.

    The proposal is presently stage 2 and has multi-vendor support. In theory the syntax is mostly stable at this point.

    There's a proposal to support it in Babel as well: babel/proposals#2

  24. benatkin commented on Mar 7, 2018

    @benatkin

    I like how TypeScript follows the spec and tries not to add too much to it, but an integer type is just so fundamental. Would love minimal integer support that's based on safe integers in ES6. Here's a concise description of it: http://2ality.com/2015/04/numbers-math-es6.html

  25. benatkin commented on Mar 7, 2018

    @benatkin

    GraphQL supports signed 32 bit integers and doesn't address other integer types. IMO that's a great starting point. http://graphql.org/learn/schema/

  26. tarcieri commented on Mar 7, 2018

    @tarcieri

    Benjamin Atkin (@benatkin) if TC39 BigInt ever ships, there's a pretty big drawback to defining integer types based on number: TC39 BigInt is a separate type at the VM level, and one which for which an exception is raised if mixed number/BigInt arithmetic is attempted.

    If people were to add int32 types to their type declarations for untyped JavaScript code which are actually applying constraints to number, and TypeScript were to switch to TC39 BigInts as the underlying JavaScript backing type for int32, then all code which has int32 in their type declarations would be broken.

    The TC39 BigInt proposal is now stage 3 and I think stands a pretty decent chance of eventually shipping, so I think it's probably best to "reserve" any potential integer types for ones which are actually integers at the VM level.

  27. ivogabe commented on Jun 8, 2018

    @ivogabe
    ContributorAuthor

    BigInt is scheduled for TS 3.0 (#15096) 🎉! Adding different syntax for "double based integers" as proposed in this issue would be confusing, so I think we can close this. See also #195.

  28. added a commit that references this issue on Oct 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    RevisitAn issue worth coming back toSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions