TypeScript
TypeScript copied to clipboard
Update expressions with untyped argument are typed `number`, not `number | bigint`
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.
Related: #41741
Okay, didn't find that one, probably because it has been closed 🤷♂️
Also related: #42125
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.
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
Dealing with this issue too! Can't do any numerical operations with using bigint
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
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
.
So if
T extends number
,T
could actually be the type1 | 2 | 3
. Then,-x
would indeed not be of typeT
.
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
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.