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

Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors #28682

Open
5 tasks done
ndbroadbent opened this issue Nov 27, 2018 · 44 comments
Open
5 tasks done
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@ndbroadbent
Copy link

ndbroadbent commented Nov 27, 2018

I have read the FAQ and looked for duplicate issues.

Search Terms

  • NaN
  • NaN type
  • Integer type

Related Issues

Suggestion

NaN has been a big source of errors in my code. I was under the impression that TypeScript (and Flow) could help to prevent these errors, but this is not really true.

TypeScript can prevent some NaN errors, because you cannot add a number to an object, for example. But there are many math operations that can return NaN. These NaN values often propagate through the code silently and crash in some random place that was expecting an integer or a float. It can be extremely difficult to backtrack through the code and try to figure out where the NaN came from.

I would like TypeScript to provide a better way of preventing runtime NaN errors, by ensuring that an unhandled NaN value cannot propagate throughout the code. This would be a compile-time check in TypeScript. Other solutions might be a run-time check added with a Babel plugin, or a way for JS engines to throw an error instead of returning NaN (but these are outside the scope of this issue.)

Use Cases / Examples

const testFunction = (a: number, b: number) => {
  if (a > b) {
    return;
  } else if (a < b) {
    return;
  } else if (a === b) {
    return;
  } else {
    throw new Error("Unreachable code");
  }
}

testFunction(1, 2);

testFunction(1, 0 / 0);

testFunction(1, Math.log(-1));

testFunction(1, Math.sqrt(-2));

testFunction(1, Math.pow(99999999, 99999999));

testFunction(1, parseFloat('string'));

A programmer might assume that the Unreachable code error could never be thrown, because the conditions appear to be exhaustive, and the types of a and b are number. It is very easy to forget that NaN breaks all the rules of comparison and equality checks.

It would be really helpful if TypeScript could warn about the possibility of NaN with a more fine-grained type system, so that the programmer was forced to handle these cases.

Possible Solutions

TypeScript could add a --strictNaNChecks option. To implement this, I think TS might need to add some more fine-grained number types that can be used to exclude NaN. The return types of built-in JavaScript functions and operations would be updated to show which functions can return NaN, and which ones can never return NaN. A call to !isNaN(a) would narrow down the type and remove the possibility of NaN.

Here are some possible types that would make this possible:

type integer
type float
type NaN
type Infinity

type number = integer | float | NaN | Infinity   // Backwards compatible
type realNumber = integer | float   // NaN and Infinity are not valid values

(I don't know if realNumber is a good name, but hopefully it gets the point across.)

Here are some examples of what this new type system might look like:

const testFunction = (a: integer, b: integer) => {
  if (a > b || a < b || a === b) {
    return;
  } else {
    throw new Error("Unreachable code");
  }
}

// Ok
testFunction(1, 2);

// Type error. TypeScript knows that a division might produce a NaN or a float
testFunction(1, 0 / 0);

const a: integer = 1;
const b: integer = 0;

const c = a + b;  // inferred type is `integer`. Adding two integers cannot produce NaN or Infinity.
testFunction(1, c); // Ok

const d = a / b;   // inferred type is `number`, which includes NaN and Infinity.
testFunction(1, d); // Type error (number is not integer)

const e = -2;   // integer
const f = Math.sqrt(e);    // inferred type is: integer | float | NaN    (sqrt of an integer cannot return Infinity)

const g: number = 2; 
const h = Math.sqrt(g);    // inferred type is number (sqrt of Infinity is Infinity)

testFunction(1, h);  // Type error. `number` is not compatible with `integer`.

if (!isNaN(h)) {
  // The type of h has been narrowed down to integer | float | Infinity
  testFunction(1, h);  // Still a type error. integer | float | Infinity is not compatible with integer.
}

if (Number.isInteger(h)) {
  // The type of h has been narrowed down to integer
  testFunction(1, h);  // Ok
}

When the --strictNaNChecks option is disabled (default), then the integer and float types would also include NaN and Infinity:

type integer      // Integers plus NaN and Infinity
type float        // Floats plus NaN and Infinity

type number = integer | float    // Backwards compatible
type realNumber = number         // Just an alias, for forwards-compatibility.

I would personally be in favor of making this the default behavior, because NaN errors have caused me a lot of pain in the past. They even made me lose trust in the type system, because I didn't realize that it was still possible to run into them. I would really love to prevent errors like this at compile-time:

screen shot 2018-11-27 at 4 35 34 pm

This error is from a fully-typed Flow app, although I'm switching to TypeScript for any future projects. It's one of the very few crashes that I've seen in my app, but I just gave up because I have no idea where it was coming from. I actually thought it was a bug in Flow, but now I understand that type checking didn't protect me against NaN errors. It would be really awesome if it did!

(Sorry for the Flow example, but this is a real-world example where a NaN type check would have saved me a huge amount of time.)

Number Literal Types

It would be annoying if you had to call isNaN() after every division. When the programmer calls a / 2, there is no need to warn about NaN (unless a is a number type that could potentially be NaN.) NaN is only possible for 0 / 0. So if either the dividend or the divisor are non-zero numbers, then the NaN type can be excluded in the return type. And actually zero can be excluded as well, if both dividend and divisor are non-zero.

Maybe this can be done with the Exclude conditional type? Something like:

type nonZeroNumber = Exclude<number, 0>
type nonZeroRealNumber = Exclude<realNumber, 0>
type nonZeroInteger = Exclude<integer, 0>
type nonZeroFloat = Exclude<float, 0>

If the dividend and divisor type both match nonZeroInteger, then the return type would be nonZeroFloat. So you could test any numeric literal types against these non-zero types. e.g.:

const a = 2;     // Type is numeric literal "2"

// "a" matches the "nonZeroInteger" type, so the return type is "nonZeroFloat" 
// (this excludes Infinity as well.)
// (Technically it could be "nonZeroInteger", or even "2" if TypeScript did 
// constant propagation. But that's way outside the scope of this issue.)
const b = 4 / a;

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. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@ndbroadbent ndbroadbent changed the title Add a strictNaNChecks option, and aNaN type Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors Nov 27, 2018
@weswigham weswigham added Suggestion An idea for TypeScript In Discussion Not yet reached consensus Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined labels Nov 27, 2018
@devlato
Copy link

devlato commented Nov 29, 2018

It would actually be great to have integer type. As discussed in #195 and #4639, it might help to force developers to do strict conversions and checks when you expect to get an integer value, eg. with math operations (as far as under the hood of JS engines, an internal conversion to integers and back takes place):

let x: integer = 1; // you expect its value to always be an integer
x = 5 / 2; // compile error
x = Math.floor(5 / 2); // ok

@reli-msft
Copy link

I think dividing number into integer and float is enough, while dealing with NaN and Infinity you need to look into the values for operators like /, while dealing with integer-float, you need only types.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 30, 2019

Shouldn't the terminology be double and not float?
I believe JS numbers are actually double precision.

Nitpick aside, an integer type that is backed by number and not bigint would have proven immensely useful for my use case.


My use case is outlined in the below link,
https://github.com/AnyhowStep/tsql/blob/adbfcf666ef71be4b6c03567a8d14a88ab699d7c/doc/00-getting-started/01-data-types.md#signed-integer

Basically, SQL has SMALLINT,INT,BIGINT types that correspond to 2,4,8 byte signed integers.

The 2 and 4 byte signed integers could have been represented by an integer type backed by number.

However, I wanted to eliminate a large class of errors where floating-point values are used instead of integer values.

So, I had no choice but to also represent 2 and 4 byte signed integers with bigint.


Also, the LIMIT and OFFSET clauses would have benefited from an integer type, so I can guarantee that numbers with fractional parts are not passed as arguments (during compile-time, anyway)


This could have been solved with branding.

However, I'm strongly of the opinion that branding should be a last resort.
Which is why I like this proposal and the range-type proposals so much.
Being able to express what we want with only built-in types reduces dependencies on external libraries for complex branding.

If a brand is used, it increases the chance of downstream users having to use multiple differing brands that mean the same thing. (Library A might use {int : void} as the integer brand, library B might use {integer : void} as the integer brand, etc)


I like the idea of having an integer type backed by number but I disagree with how the OP wants the type to be defined.

I would prefer,

//Infinity is now a literal type
declare const inf : Infinity;

//-Infinity is now a literal type
declare const negInf : -Infinity;

//NaN is now a literal type
declare const nan : NaN;

//No change to this type
type number;

type finiteNumber = number & (not (Infinity|-Infinity|NaN));

type integer; //All **finite numbers** where Math.floor(x) === x

I do not like the idea of Infinity, -Infinity, and NaN being part of the integer type. It is very unintuitive and usually not what I (and I assume others) would want.

If someone wants the original proposal, they can just create their own type alias,

type PotentiallyNonFiniteInteger = integer|Infinity|-Infinity|NaN;

With my proposal, number would be the same as finiteNumber|Infinity|-Infinity|NaN.

We would get the following type guards,

  • isFinite(x) : x is finiteNumber
  • isNaN(x) : x is NaN
  • Number.isInteger(x) : x is integer

Also, another reason to prefer my proposed definition for integer is that it follows the Number.isInteger() convention,
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill

Number.isInteger = Number.isInteger || function(value) {
  return typeof value === 'number' && 
    isFinite(value) && 
    Math.floor(value) === value;
};

[UPDATE]
I wrote an update earlier on but it got lost to the void because GitHub error'd. Ugh.
I am retyping my update below.


Anyway, I re-read the OP and realized I had misunderstood parts of it. The integer proposal I disagree with would only be applied if strictNanChecks is turned off.

I am against the new flag being introduced.

New flags should only be introduced when intentionally breaking changes are introduced. These new types would not break the current behaviour of the number type.

If anyone uses integer,finiteNumber,Infinity,-Infinity,NaN (new types), they should be forced to acknowledge the possibility of Inifinity,-Infinity,NaN,etc. values creeping in to their code and handle it accordingly (with type guards, run-time errors, etc.)

If they do not want to have the compiler warn them about potentially undesirable values, they can stick to using number and live blissfully unaware of impending doom.

@MaxGraey
Copy link

MaxGraey commented Nov 11, 2019

It will be great also suggest replace x === NaN to Number.isNaN(x) or isNaN(x) during diagnostics because x === NaN always false and 100% mistake.

@benwiley4000
Copy link

benwiley4000 commented May 5, 2020

Has this issue been picked up by the maintainers? It's surprising to me that a type-checking compiler for JavaScript wouldn't try to prevent one of the most notorious type issues prevented by nearly every other language, dividing by zero (not sure if TS would need zero and non-zero subtypes in order to be able to typeguard a valid division).

EDIT: I guess I should add that other languages don't necessarily prevent this at compile time, but they throw errors when it happens. JavaScript fails silently, so it would be great for TypeScript to be able to be able to detect this. But I understand that it requires some significant modifications to the type system.

@captain-yossarian
Copy link

It would be greate to see this new rule in TypeScript 4.0

@JasonHK
Copy link

JasonHK commented May 25, 2020

Since there is a function called Number.isFinite(), I suggest use finite instead of realNumber as the name of the type for real numbers.

@ghost
Copy link

ghost commented Oct 10, 2020

When I started really learning TS, I expected that at least NaN and Infinity would have their own types, but alas no, they do not, they are simply of the type number.

Also, arrays really ought to be Array<T> = { [key: integer]: T }.

We can all agree this is great, totally bug-free code, right?

const temp: string = ["temporary value"][Infinity];

console.log(temp);

Who could have expected undefined!?

@RyanCavanaugh
Copy link
Member

n.b. Integer types have already been decided as out of scope (especially now that BigInt is a thing), so what follows only refers to NaN typing. We discussed this at length and couldn't come up with any plausible design where the complexity introduced here would provide sufficient value in most use cases.

The only reasonably common way to get a NaN with "normal" inputs is to divide by zero; programs that do standard arithmetic should be on guard for this and add appropriate checks the same way you would for handling file existence, lack of network connectivity, etc (i.e. things that are common errors but not enforced by a type system). Once a well-formed program correctly guards its division, any NaN checking that follows is mostly going to be noise. For example, if you write Math.log(arr.length), this can never return NaN, but we only know this because arr.length can't be negative. Many other functions work in a similar way - to provide these NaNs in useful places we'd have to encode a large amount of data about possible values for numbers (integers, positive, nonnegative, less than 2^32, etc) all over the type system, which is a ton of work that everyone would have to reason about from that point forward for the sake of the relatively small number of programs that encounter unexpected NaNs due to logic errors.

@calimeroteknik
Copy link

Well, it's an all-or-therewillalwaysbeshenanigans choice.
I was hoping TS might aim for complete correctness by doing all of the above; perhaps the answer is "not now", then?

Easy comes with complications; simple comes with complexity.

@ExE-Boss
Copy link
Contributor

At the very least, NaN, Infinity and -Infinity should be valid and correctly handled in type positions, since TypeScript can and will emit Infinity and ‑Infinity in type positions: #42905.


This would also make it possible for TypeScript to produce an error for code like the following:

declare var foo: number;

// @ts-expect-error
if (foo === NaN) {
	// ...
}

Then TypeScript warns that === NaN will always return false and !== NaN will always return true.


Similarly, Number.isNaN could then be defined as:

interface NumberConstructor {
	isNaN(number: unknown): number is NaN;
}

There’s also real world code that relies on returning NaN in a catch clause: https://github.com/engine262/engine262/blob/ab4baf1b1faeae35da022aea6c60a6a824822167/src/abstract-ops/type-conversion.mjs#L404-L415, because the error is handled elsewhere: https://github.com/engine262/engine262/blob/ab4baf1b1faeae35da022aea6c60a6a824822167/src/abstract-ops/type-conversion.mjs#L386-L394.

@ca-d
Copy link

ca-d commented Mar 17, 2021

We need this in order to specify a function that has to return NaN on purpose in specific cases. If we returned another number, like 0, we could declare it as:

function example(): string | 0 {
...

but sadly we can currently not declare the function as returning:

function example(): string | NaN {
...

so we have to fall back to the very loose definition

function example(): string | number {
...

@ghost
Copy link

ghost commented Mar 17, 2021

@c-dinkel Are there any particular reasons that you're using NaN for that value? Could you elaborate on what your function actually does, and in what case it is returning NaN? Is it your function or just a type definition for an arbitrary JS function? Would you be willing to consider string | null instead, or throwing an exception?

@ExE-Boss
Copy link
Contributor

I have encountered code like @c-dinkel’s in the wild as well, so having a NaN type would help.

@calimeroteknik
Copy link

calimeroteknik commented Mar 17, 2021

While I don't think it is good to implement such fundamental language features on an as-needed basis[1], the question @crimsoncodes0 asks does deserve a proper answer, which was not given in the reply above.

  1. It is useful to be able to prove that a function does not return NaN. I think that needs no justification but here it is for completeness: this would allow, compile-time, to avoid hard-to-debug runtime crashes that are due to a division by zero (or other) that produces NaN, and causes a confusing error very far away from the code that creates this NaN value initially.

  2. It is useful to be able to tell the compiler we do want a function that can return NaN, exactly when we want to handle it later, not here, in a function whose type is exactly one that disallows NaN as a return value.

Errare humanum est. Without strict checks, Murphy's law reigns!

[1] This is because all code written before the availability of basic features in the language will have to use workarounds or be unsafe, and will need to be rewritten when the language fixes that flaw, which it now needs to do with some amount of backwards compatibility with the workarounds in question. All that code working around missing language features will not be refactored using the new feature right away, or at all, because that has a cost. All in all, introducing language features as the need arises creates technical debt orders of magnitude higher, as well as "good practices" that will be obsoleted by introducing the language features proper.

@ca-d
Copy link

ca-d commented Mar 19, 2021

@c-dinkel Are there any particular reasons that you're using NaN for that value? Could you elaborate on what your function actually does, and in what case it is returning NaN? Is it your function or just a type definition for an arbitrary JS function? Would you be willing to consider string | null instead, or throwing an exception?

While in this particular case a value is needed that compares falsely to itself, there are other cases where NaN is necessary for other reasons. Imagine, for example, a numeric calculation that returns 0 | 1 | 2 | 3 | NaN , I'd really want to be able to express that.

And no, I'm not willing to consider changing the runtime semantics of the program just to make tsc happy at compile time. Also, what if I'm trying to add types to an external library I don't control?

@SheepTester
Copy link

Regardless of whether an integer type is added, I don't think this form of NaN checking, where two operands known to not be NaN produces a non-NaN number, would be perfect. For example, adding two integers doesn't necessarily produce another integer:

const int: number = 1e308
if (Number.isInteger(int)) { // This is true
  // int: integer
  const sum: integer = int + int // At runtime, Infinity
  const difference: integer = sum - sum // At runtime, NaN
}

I think it's not a big enough deal though since TypeScript is already unsound—['a'][1].length has no type errors but throws a TypeError because having undefined checks for array access is annoying—and the type systems of most other languages such as Rust don't check for integer overflows, so there's not really a need to require checks on the programmer's side for large floating point integers

Also, using the name float or double to mean "not NaN nor an Infinity" might be misleading because NaN and the Infinities are valid floating point values in other programming languages. I'm not really sure what values would be a realNumber but not a float, but I think using realNumber/real or finite would probably be a better name for a such a set of numbers

@ljharb
Copy link
Contributor

ljharb commented Jan 8, 2022

I believe this would help solve #47347 / #15135.

@yuhr also, 1 / -0 !== 1 / 0. -0 exists in JS, it's a thing.

@ca-d
Copy link

ca-d commented Jan 11, 2022

@c-dinkel Are there any particular reasons that you're using NaN for that value? Could you elaborate on what your function actually does, and in what case it is returning NaN? Is it your function or just a type definition for an arbitrary JS function? Would you be willing to consider string | null instead, or throwing an exception?

@crimsoncodes0 you asked all these clarification questions about our use case last year, were they all answered in my comment above? Have you been satisfied by my explanation? If so, what is your suggestion in our case?

@anematode
Copy link

@SheepTester made an excellent point above... there's no sound way to have an integer type or finite type without checking nearly every operation and/or doing sophisticated range analysis. The four main binary floating-point operations can over- or underflow; the only practicable operations to check are those which cast to signed 32-bit.

Despite this limitation, I would really like to see NaN as a type literal, just as an annotation. NaNs are quite useful signaling tools in numeric code.

@calimeroteknik
Copy link

@anematode well there are two objectives; I'll concentrate on avoiding runtime type errors caused by NaN. Checking every operation is necessary, and this is exactly what TypeScript does by construction: operations (operators) are functions, and TS checks that function signatures match their calls. That's actually already the main selling point of TS.

Thankfully, new compiler features like range analysis can remain out of the picture to do this.

This can be implemented only by introducing a more stringent number type or compiler option and adding a few signatures, which will in turn make TS reject programs that lack the proper guard conditions before potentially dividing by zero and so on.

The only reason I can imagine TS maintainers getting cold feet at the idea, is that at first glance they might think it will result in several operation modes and break backwards compatibility, but with new types it doesn't have to!

@MilesBHuff
Copy link

MilesBHuff commented Jul 29, 2022

When dealing with user input, it's not uncommon to do parseFloat and get NaN because the user inputted some non-numerical value. If parseFloat had a return type of number | NaN, our more-junior developers wouldn't be able to forget to check for NaN in user input.
(While much of the above can be avoided by restricting the user to inputting only numeric characters into the field, parseFloat(''), parseFloat(null), and parseFloat(undefined) remain possible.)

There's also just that I don't like to have to do const foo: number | null = null, because typeof null === 'object'. I'd much rather do const foo: number | NaN = NaN.

@nicolas377
Copy link
Contributor

Since numbers are out of scope of TS, here's a playground with a potential workaround. I'm currently writing a library to .d.ts polyfill all the methods that can potentially return NaN, along with Number.isNaN() and the global isNaN().

@avin-kavish
Copy link

avin-kavish commented Aug 2, 2022

I think this is pretty broken atm,

type Falsy = 0 | '' | false | null | undefined | -0 | 0n // | typeof NaN <-- doesn't work

typeof NaN resolves to type number but it is not a number,

Also, to guard against truthy values, I had to create the Falsy type alias (which doesn't work, cause NaN isn't literally typeable)

export function assertFalsy(value: unknown): asserts value is Falsy {
  if (value) throw new AssertionError('Expected falsy value')
}

Can't we just write asserts not value or asserts !value?

Context of why this matters - Probably in new code you would add null | undefined to a type and then check for it under strict: true, then this won't matter, but I'm migrating a legacy code base that very loosely checks types and uses a lot of implicit conversions.

@muhamedkarajic
Copy link

Is this anywhere close to being fixed?

@ca-d
Copy link

ca-d commented Apr 5, 2023

Is this anywhere close to being fixed?

Nope, after lots of time and many issues this is apparently not considered an actual issue by the maintainers. If you absolutely need this fixed, consider forking TypeScript.

@KarolS
Copy link

KarolS commented May 25, 2023

I think this is unsolvable, as a+a is infinity and (a+a)-(a+a) is NaN when a = Number.MAX_VALUE (which is an integer). So NaNs and infinities can creep up into any ordinary integer-only code. You couldn't call a function passing an arithmetic expression to an integer parameter without casting, so you'd have to do things like f((i+1) as integer), which kinda defeats the purpose of types.

Alternatively, you could define types of arithmetic operations to be unsound (i.e. return integer for integers and ignore overflows), but I don't think Typescript needs any more unsoundness.

The only things that could be safely done are:

  • a type named NotFraction, which would contain integers, infinities and NaNs – preserved by +, -, ++, --, *, |, &, ^, ~,
  • a type named NonnegativeNumber, which would contain nonnegative finite numbers (including -0) and positive infinity – preserved by + and ++ (in particular, not preserved by *, / or **, as 0*Infinity, 0/0 and 1**Infinity are NaN),
  • analogous NonpositiveNumber (including +0), preserved by + and --,
  • a type named PositiveNumber, which would contain positive finite numbers (excluding +0) and positive infinity – preserved by +, ++ and * (in particular, not preserved by **, as 1**Infinity is NaN),
  • analogous NegativeNumber (excluding -0), preserved by + and --, and with NegativeNumber * NegativeNumber = PositiveNumber,
  • NonnegativeNotFraction = NotFraction & NonnegativeNumber, etc.
  • a type named NaN containing NaN, just so Falsy could be defined.

Note there's no way to guarantee lack of infinity, and no way to have subtraction, division or modulo without NaNs.
Also, I don't think those types should ever be inferred.

Of course I could have made some mistakes above, this is not a serious proposal, as I don't think those types would be very useful.

@bbrk24
Copy link

bbrk24 commented May 25, 2023

You can actually do even more than that: there's int32 which is closed on &, |, ~, ^, <<, and >>, and uint32 which is closed on <<< and >>>. These are totally NaN- and Infinity-safe: NaN << NaN is 0 and 1 << Infinity is 1.

@muhamedkarajic
Copy link

I just want to be able when I write 1.0 and its not suppossed to be float to get an error in typescript and i want if the type is float to have to make it 1.0. Is that not possible?

@SheepTester
Copy link

I just want to be able when I write 1.0 and its not suppossed to be float to get an error in typescript and i want if the type is float to have to make it 1.0. Is that not possible?

It's idiomatic in JavaScript to just use 1 instead of 1.0 even in cases where it's used as a real number rather than an integer (e.g. setting image quality, media volume, canvas shapes, etc). Having to use 1.0 for floats is an idiom from some other programming languages that shouldn't be forced onto TypeScript users for no good reason. We already have special syntax for bigints, anyways

@rschristian
Copy link

Having to use 1.0 for floats is an idiom from some other programming languages that shouldn't be forced onto TypeScript users for no good reason.

The "good reason" is allowing users to take advantage of a type system.

A frequent problem I'm running into is that, with WASM, there's no way to signal to consumers in JS whether an interface accepts signed/unsigned integers, floats, etc. Users have to read the source or guess and check as TS can't provide any useful info beyond "it's a number of some sort", which is a shame. Being able to specify, precisely, what kind of number is allowed is really important as TS branches out to describe interfaces beyond plain JS.

@KarolS
Copy link

KarolS commented Feb 3, 2024

Being able to specify, precisely, what kind of number is allowed is really important as TS branches out to describe interfaces beyond plain JS.

I wonder if the type system is the right place for that though. Constraining WASM interfaces is a complex problem, not fitting the weird JS type system. As previously said, you cannot have an integer type that doesn't immediately dissolve into nonsense.

So what about: compile-time preconditions for function parameters, which can reject incorrect constant arguments? This would allow for many other compile-time checks, like "require a prime number", "require a list of length divisible by 4", "require two fields of the object to be different" and so on.

Extend function types with another component: precondition, which is an arbitrary TS expression. Only a reasonable subset of TS needs to be supported, and its environment contains only the parameters and the standard library. When typechecking a call to a function with a precondition, plug all the parameters into the expression, assuming unknown value for non-constant parameters, and evaluate it taking into account interactions between known and unknown values (so it cannot be just a simple JS eval). If the result is a falsy constant, report compilation failure:

// WARNING: REALLY SILLY NOT-ACTUALLY-A-PROPOSAL FOLLOWS:
declare function requires_i32(i:number, j:number):void havingPrecondition (i === (i|0) && j === (j|0)) ;
export let not_a_constant = 1.2;
requires_i32(0, 0); // ok
requires_i32(0, not_a_constant); // ok, because who knows what's inside that variable
                                 // true && unknown_value is unknown_value according to the precondition evaluator,
                                 // which is not a falsy constant, therefore it compiles fine 
requires_i32(0, 3.14); // definitely not ok
requires_i32(not_a_constant, 3.14); // definitely not ok – even if we cannot evaluate the precondition like normal code, 
                                    // unknown_value && false is still false according to the precondition evaluator

Just a throw-away idea, I think it's a bit too complicated to implement.

@mharj
Copy link

mharj commented Mar 15, 2024

This would help a lot to see parseInt problems on code, as we just managed to create bug on our code as developers still expecting parseInt actually throw Type error if not valid (or expect undefined). Of course we can build custom parseInt function to handle this, but on large codebase actual NaN return type would already help to identify those issues.

@Akindin
Copy link

Akindin commented Nov 8, 2024

As previously said, you cannot have an integer type that doesn't immediately dissolve into nonsense.

Even if they cannot be implemented on most of build in operators, they are more than useful in libraries and custom utilities. Even an ability for it to understand that integer is assignable to float and float is assignable to number is a huge improvement so you don't need to make a branded types yourself and cast each time you write const a = 5 as integer to pass it into function. isSafeInteger, isInteger, isFinite, isNaN will work like a typeguard, Math.floor, also in place where NaN can be, you can use NaN || 0.
Also ++ and -- won't go into infinity.

@Akindin
Copy link

Akindin commented Nov 8, 2024

Also it will be good to restrain from using numbers out of the Date range, out of Radix range, out of toFixed argument range and array range and safeNumber range, at least on a level of constants, because simple for loop with increment can turn into infinite loop

@Rudxain
Copy link

Rudxain commented Nov 19, 2024

havingPrecondition

Refinement types would be amazing (actually, more than just "amazing"). See https://arxiv.org/pdf/1604.02480.pdf

But (as previously mentioned) the predicate language must be a subset of TS, to forbid Turing-Completeness (we already have it #14833), or at least remove some "useless" functions and operator semantics (for basic sanity). Or maybe the predicate-lang could be a different lang altogether (other langs did something similar with macros)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests