Skip to content

Update expressions with untyped argument are typed number, not number | bigint #49558

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

Open
fwienber opened this issue Jun 15, 2022 · 10 comments
Open
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@fwienber
Copy link

Bug Report

Update expressions (++, --) with an untyped (any) argument are typed as number, not as number | bigint.
This results in missing and incorrect type errors.

🔎 Search Terms

label:Bug bigint

🕗 Version & Regression Information

Support for bigint has been introduced with TypeScript 3.2.
The bug is still present in TypeScript 4.7.2 and today's Nightly (15/06/2022).
It seems the edge case this issue describes has just been overlooked.

⏯ Playground Link

Playground link with relevant code

💻 Code

const anyInc1 = (myUntypedBigInt: any): number => ++myUntypedBigInt; // bug: no type error
const anyInc2 = (myUntypedBigInt: any): bigint => ++myUntypedBigInt as bigint; // bug: type error

🙁 Actual behavior

For anyInc1, there is no type error, although the result of the expression at run-time may be a bigint when the argument is a bigint (e.g. anyInc1(42n)).

For anyInc2, there is the type error

Conversion of type 'number' to type 'bigint' may be a mistake because neither type sufficiently
overlaps with the other. If this was intentional, convert the expression to 'unknown' first.(2352)

although the correct type of the update expression, number | bigint, does overlap with bigint.

🙂 Expected behavior

The resolved type of applying any update expression operator (++, --, pre- or postfix) on an untyped expression should be number | bigint, not number.

Then, anyInc1 would result in the correct type error

Type 'number | bigint' is not assignable to type 'number'.
  Type 'bigint' is not assignable to type 'number'.(2322)

As a side-note, using number | bigint instead of any as the parameter type correctly results in exactly that type error:

const typedInc = (myNumberOrBigInt: number | bigint): number => ++myNumberOrBigInt; // type error

anyInc2 would no longer result in a type error.

@MartinJohns
Copy link
Contributor

Related: #41741

@fwienber
Copy link
Author

Okay, didn't find that one, probably because it has been closed 🤷‍♂️

@fwienber
Copy link
Author

Also related: #42125

@fwienber
Copy link
Author

The part of #47741 that is related: The problem described here for update expressions (++, --) also seems to affect the unary - operator.

const anyNegate1 = (myUntypedBigInt: any): number => -myUntypedBigInt;

also does not give a type error, although it should, while

const anyNegate2 = (myUntypedBigInt: number | bigint): number => -myUntypedBigInt;

correctly reports a type error.

@yukulele
Copy link

yukulele commented Dec 3, 2023

Almost all numerical operators are affected.

Given that:

let x: any = 0n

All these statements return the type number, they should return the type number | bigint:

-x ~x
++x --x x++ x--
x - x x * x x / x x % x x ** x
x << x x >> x x & x x | x x ^ x
x -= x x *= x x /= x x %= x x **= x
x <<= x x >>= x x &= x x |= x x ^= x

These statements return the type any, they should return the type string | number | bigint:

x + x x += x

This statements returns the type number which is correct because +0n throws a TypeError:

+x

playground

@Blackglade
Copy link

Dealing with this issue too! Can't do any numerical operations with using bigint

@Rudxain
Copy link

Rudxain commented Jun 19, 2024

This is also a problem with generics:

// ❌
const f = <T extends number | bigint,>(x: T): T => -x

// ✅
const g = <T extends number | bigint,>(x: T) => -x as T

The type-assertion is silly

Rudxain added a commit to Rudxain/RX-wiki that referenced this issue Jun 19, 2024
@fwienber
Copy link
Author

fwienber commented Jul 9, 2024

This is also a problem with generics:

// ❌
const f = <T extends number | bigint,>(x: T): T => -x

// ✅
const g = <T extends number | bigint,>(x: T) => -x as T

The type-assertion is silly

Sorry to disagree, but it isn't. This error is not related to bigint, but to the extends clause.
Even if you remove the | bigint part, you still get the same type of error:

Type 'number' is not assignable to type 'T'.
'number' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'number'.(2322)

So what happens here? In TypeScript, there are subtypes of number, namely all number literal types and all union types of these.
So if T extends number, T could actually be the type 1 | 2 | 3. Then, -x would indeed not be of type T.

Note that

const f = (x: number|bigint): number|bigint => -x;

does not result in a type error, but you could argue that it is unprecise, because it doesn't state that a number leads to a number, and a bigint leads to a bigint.

Thus, I'd suggest to use an overloaded function in this case, like so:

function f(x: number): number;
function f(x: bigint): bigint;
function f(x: number|bigint): number|bigint {
  return -x;
}

Then, f(1) is correctly deducted to be of type number, while f(1n) is of type bigint.

@Rudxain
Copy link

Rudxain commented Jul 10, 2024

So if T extends number, T could actually be the type 1 | 2 | 3. Then, -x would indeed not be of type T.

Correct! That was an oversight on my part

Thus, I'd suggest to use an overloaded function

Thanks for the info! I've already known about overloads, but I decided to avoid them because I thought generics were "better practice" than overloads. Being a Rust dev doesn't help, as we're forced to use generics

@fwienber
Copy link
Author

Thus, I'd suggest to use an overloaded function

Thanks for the info! I've already known about overloads, but I decided to avoid them because I thought generics were "better practice" than overloads. Being a Rust dev doesn't help, as we're forced to use generics

If you want to avoid overloads and still want to declare precise input->output type mappings, you can use conditional types instead. In the example from above, this would look like so:

function f<T extends number|bigint>(x: T): T extends number ? number : bigint {
  return -x as any;
}

I actually prefer overloads in most case, because even though they are lengthy, they make the different usages of the same function clearer, and you can even add different TSDoc to each overload, while with conditional types, the one signature quickly becomes complex and you have to explain all usages in one documentation block. But it really depends on the use case and on personal taste.

Also, as you can see in the example, implementing a function with a conditional return type often results in having to use an as any type assertion, which I think is quite ugly, as it bypasses type checks. At least I found no other way to convince TypeScript that my implementation does what the conditional type requires it to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants