microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.03k stars 12.49k forks source link

Allow Inifinity and -Infinity as number literal types #32277

Open smikitky opened 5 years ago

smikitky commented 5 years ago

Please accept Infinity and -Infinity as valid number literal types.

/** A range of BigInt that may be unbounded, e.g., (-∞, 5] */
class BigIntRange {
  public min: bigint | -Infinity = 0n; // <= Error!
  public max: bigint | Infinity = 1n; // <= Error!
  public isValid(): boolean {
    return this.min <= this.max;
  }
}

Currently, This causes a compile error, 'Infinity' refers to a value, but is being used as a type here. (2749).

I understand this was once rejected as part of #15135 and #15356, and the given reasons were: 1) difficult to implement, and 2) lacks a compelling use case.

However, while I understand why NaN as a literal type is tricky to implement, NaN and Infinity are not the same. And I have a clear use case for Infinity as a meaningful literal type, as shown above.

Infinity is not tricky to compare

Unlike notorious NaN or -0, Infinity works just like an ordinary numeric constant as far as equality is concerned. We don't need a special function like isNaN() or Object.is(). Ever since the ES5/IE6 era, there has been nothing counter-intuitive:

Infinity === Infinity // => true
-Infinity === -Infinity // => true
Number.POSITIVE_INFINITY === Infinity // => true
Object.is(-Infinity, Number.NEGATIVE_INFINITY) // => true
10 ** 500 === Infinity // => true (because 10**500 is above Number.MAX_VALUE)

typeof Infinity === 'number' // => true
typeof Number.NEGATIVE_INFINITY === 'number' // => true

// FWIW, comparison works just as expected, too
50 < Infinity // => true
-Infinity < 50; // => true
10n ** 500n < Infinity // => true
Number.MAX_VALUE < Infinity // => true
Infinity < Infinity // => false

Most importantly, Infinity === Infinity is true (while NaN === NaN is false). Unless I'm missing something, predictable equality is all that's required to safely use Infinity as a literal type, right? Even though the design note (#15356) says "there is more than one NaN, Infinity, etc", you can think of Infinity in JavaScript as "just a fixed number" which happens to be larger than Number.MAX_VALUE.

My use case

My library deals with unbounded (aka infinite) integer ranges, and I have been using Infinity and -Infinity without any issue to denote what they literally mean, infinity in the mathematical sense. I have instructed my users to use these interesting constants, too. Recently I started to extend my library to support bigint in addition to number, and ran into this problem.

You may ask "Why don't you just use string 'Infinity' or Symbol('unbounded')", but Infinity is a predefined and predictable constant, and it can be directly compared with any bigint (e.g., 10n ** 1000n < Infinity is true). See how simple the implementation of isValid can be in the first example.

PS: Looks like there is a hacky workaround (#31752), but I'd like to see the official support.

poseidonCore commented 5 years ago

Also, to state the obvious in favour of this, typeof Infinity = "number", not some odd object.

Quizzically, Infinity has some odd type conversions internally that may be at the heart of this:

TS Playground ts3.5.1

type numberNames = {0: "zero", 1: "one", Infinity: "inf"};
type numberTypes = keyof numberNames;

// These are fine:
var zero:numberTypes = 0;
var one:numberTypes = 1;

// This is an expected error:
var two:numberTypes = 2;
/* ERROR
var two: 0 | 1 | "Infinity"
Type '2' is not assignable to type '0 | 1 | "Infinity"'.
*/

// This is an unexpected error:
var inf:numberTypes = Infinity;
/* ERROR
var inf: 0 | 1 | "Infinity"
Type 'number' is not assignable to type '0 | 1 | "Infinity"'.
*/

Here, Infinity is treated as a string ... not a number, unlike the other number keys.

So, Infinity might be treated just like any other simple string key here, at least.

AnyhowStep commented 5 years ago
const Inf = 999e308;
type Infinity = 999e999999; //Just for fun, it's still the same as `Inf`

//We use `Inf` instead of `Infinity`
type numberNames = {0: "zero", 1: "one", [Inf]: "inf"};
type numberTypes = keyof numberNames;

// These are fine:
var zero:numberTypes = 0;
var one:numberTypes = 1;

// This is an expected error:
var two:numberTypes = 2;
/* ERROR
var two: 0 | 1 | "Infinity"
Type '2' is not assignable to type '0 | 1 | "Infinity"'.
*/

// OK!
var inf:numberTypes = Infinity as Infinity;

Playground

shreyasminocha commented 4 years ago

I have a very similar use case and was surprised that this doesn't work.

appsforartists commented 3 years ago

Also prevents you from making Infinity a mandatory key in a Record:

export type ImageSrcSet = Record<number, string> & { [Infinity]: string };

I'd expect that to resolve to a numeric dictionary where all keys are optional except for Infinity. Instead, it triggers TS1170: a computed property name must be a literal type.

GuiltyDolphin commented 3 years ago

I agree it makes sense to add Infinity and -Infinity - these could be treated as top/bottom elements (pity we don't have orderings in types). And as with the reference from #44408, some operations have special behaviour for Infinity, so it would be handy to be able to describe this behaviour in the types.

IanBellomy commented 3 years ago

Additional silly (?) use case:

Consider a roleplaying game where players roll a dice against an unconstrained difficulty number.

You'd like a type that includes the possible die results plus an 'auto-succeed' value for peculiar circumstances, e.g.

type D6 = 1|2|3|4|5|6|Infinity
Rudxain commented 2 years ago

Another use case is to have richer type/value checking. I have this module where this feature would certainly be helpful for any library-API user (usually, only myself, haha)

MichalMarsalek commented 1 year ago

I also ran into this when implementing unbounded ranges, hoping that bigint | Infinity would be possible.

smikitky commented 9 months ago

I have been waiting for this for over 4 years.

Unless I'm missing something, (-)Infinity is the only distinct primitive value that TypeScript cannot correctly treat in a literal type context. Aside from NaN (which is not "distinct" in the first place), all primitive values, including bigints and symbols, can be correctly handled as a literal type. I personally think this is more of a defect than a feature request.

Infinity is the only member of the number type that is guaranteed to be larger than any bigint, which is important in some practical scenarios. Infinity is not some "error value" that arises from some unexpected calculation result, but is a fully-fledged constant whose behavior is strictly and rationally defined in the world of numerical computation in JavaScript.

olsonpm commented 3 months ago

My usecase for Infinity is a "flatten" utility accepting a depth of number including Infinity. It would be nice to overload for the case of depth: Infinity. Creating a separate function or signature for this case would be expanding the api only to appease typescript

camsteffen commented 1 month ago

As long as Infinity exists in JavaScript, I think people will find reasonable use cases for it from time to time, and TypeScript is limiting its utility for no good reason.

Today I wanted to write a function like

expiry(): Date | Infinity