Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fake Operator Overloading

Tags:

typescript

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.

like image 990
Mike Lischke Avatar asked Jul 23 '26 19:07

Mike Lischke


1 Answers

The behavior you want, whereby TypeScript allows you to use a custom class that overrides the Object.prototype.valueOf() method as if it were the primitive type returned by valueOf(), is unfortunately not part of the language. There's a fairly longstanding open feature request for it at microsoft/TypeScript#2361, but it has not been implemented, and it doesn't look like it will be implemented anytime soon.

For now, that means, there are only workarounds. One workaround you could use is to lie to the compiler that Long has a construct signature that returns a primitive instance. That is, you want it to have the type new (value: bigint | number) => bigint; (or perhaps new (value: bigint | number) => bigint & Long so that you keep any extra methods or functionality added to Long).

Here's how you could do that:

// rename
class _Long { 
    public constructor(private value: bigint | number) { }

    public valueOf(): bigint {
        return BigInt(this.value);
    }
}

// assign and assert
const Long = _Long as 
    new (value: bigint | number) => bigint & _Long;

Here I've renamed your original Long constructor out of the way, because once you declare class Long { } the value Long gets a constructor type automatically, and you can't change that type.

Then I assign the renamed constructor to the desired variable named Long, and asserted that it is of the desired type, new (value: bigint | number) => bigint & _Long instead of the actual type, new (value: bigint | number) => _Long.

I need to use a type assertion because the compiler would complain about a plain assignment; it knows that Long's instance type is not bigint.


Okay, so now we have a class constructor named Long that the compiler thinks produces bigint instances. Let's test that:

const long = new Long(123);
// const long: bigint
console.log(456n + long); // okay, 579

Looks good. I can call new Long(123) and the compiler thinks the result is a bigint. It also lets me use mathematical operators like + without complaint, and the result is what you expect.


So that works about as well as I can imagine. Still, I would generally not intentionally lying to the compiler, since such lies can trip you up later in weird ways. The type of long is observably not a bigint:

console.log(typeof long); // "object", not "bigint"

And so any operation that depends on long actually being a bigint could do funny things the compiler can't catch so you'll see unexpected runtime behavior:

const bigint1 = BigInt(4);
const bigint2 = BigInt(4);
console.log(bigint1 === bigint2); // true

const long1 = new Long(4);
const long2 = new Long(4);
console.log(long1 === long2); // false!

function copy<T extends {}>(x: T) {
    return (typeof x === "object") ? { ...x } : x
}

const bigint3 = copy(bigint1);
console.log(bigint3 + 1n) // 5

const long3 = copy(long1);
console.log(long3 + 1n) // "[object Object]1" 🤪

Now in the particular use case mentioned in the question, since all of the TypeScript code will be generated, you might be able to guarantee that you don't generate any code that trips over any of these stumbling blocks. But even so, it's important to be aware of these things and take them into account when deciding whether you want to proceed.

Playground link to code

like image 106
jcalz Avatar answered Jul 26 '26 11:07

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!