Issue
JS/TS does automatic boxing and unboxing of String
, Number
and Boolean
types, which allows to use a mix of literals and objects in the same expression, without explicit conversion, like:
const a = "3" + new String("abc");
I'm trying to implement something similar for bigint
and number
by providing a custom class Long
:
class Long {
public constructor(private value: bigint | number) { }
public valueOf(): bigint {
return BigInt(this.value);
}
}
const long = new Long(123);
console.log(456n + long);
This works pretty well (and prints 579n
), but causes both, my linter and the TS compiler to show errors for the last expression. I can suppress them with comments like this:
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
console.log(456n + long);
but that's not a good solution for entire apps.
Is there a way to tell that Long
is to be treated as a bigint
or anything else to avoid the errors?
About Why Doing That:
I'm working on a tool to convert Java to Typescript and want to support as many of the Java semantics as possible. The type Long
holds a long
integer, which is 64 bit wide, which can only be represented in TS by using bigint
. The main problem with that is that Java automatically unboxes Long
just like String
and I want to support this semantic as far as I can.
For @caTS: so this will never be normal TS code but always used as java.lang.Long
and hence there will be no confusion.
Solution
Beside the problems @jcalz already mentioned there's a big issue with his solution: you cannot use any of the functionality of the Long
class. For the compiler as well as ESLint Long
is treated as the bigint
primitive type due to that additional constructor assertion.
However, JS has a built-in primitive resolution approach, described on the Symbol.toPrimitive page. In my original approach I used one of the solutions (valueOf
) to let JS automatically convert the Long
class to the primitive bigint
value, which preserves all the Long
class functionality.
Unfortunately, using the valueOf
or Symbol.toPrimitive
still require an explicit step (invoking either numeric, string or primitive coercion), so it's only a semi-perfect solution. Here's the class using this approach:
class Long {
public constructor(private value: number) {
}
public valueOf(): number {
return this.value;
}
private [Symbol.toPrimitive](hint: string) {
if (hint === "number") {
return this.value;
} else if (hint === "string") {
return this.toString();
}
return null;
}
}
const long = new Long(123);
console.log(456 + +long);
(playground). Note the extra +
in front of the long
variable, which triggers the numeric coercion. As you can see I have not used bigint
here, because that is treated in a different way. In the original code no numeric coercion was necessary to make it work (only error suppression). Using Symbol.toPrimitive
with bigint
does not work, however, even with numeric coercion. It's simply not implemented. A proposal to add that functionality was refused years ago.
So, in summary, for bigint
there's no solution to my question (and bigint
wasn't a good choice anyway). But for all other classes which can be coerced to a primitive type (either string or number) the mentioned approach works well, if you accept the explicit coercion.
Update
I played with the suggestions from @jcalz to use a separate intersection type for the Long
class. Check this new playground example for details. While this works apparently, it still shows a number of typescript errors, which I would have to suppress. Essentially all static members raise a TS error.
Additionally, there are other problems with this solution, like the changed class name (which is no longer LONG
) when using constructor.name
(which I have to use for reflection emulation).
Answered By - Mike Lischke
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.