ts-rs icon indicating copy to clipboard operation
ts-rs copied to clipboard

Feature request: `bnum` compatibility

Open aumetra opened this issue 1 year ago • 2 comments

Is your feature request related to a problem? Please describe.

In my exploration in adopting ts-rs for TypeScript typegen in a project, I hit the roadblock of needing a bnum implementation.

Describe the solution you'd like

An implementation for the bnum structs would be nice, but there is something to discuss here, which is why I opened an issue here first.


First off, here's a link to the documentation of the crate: https://docs.rs/bnum/latest/bnum/

While crate is made for arithmetic, it is made for arbitrary precision arithmetic.
Now here's the thing, should the type for bnum be expressed as a number, a string, or some binary format.

The serde impl seems to serialize it as an array of limbs, but in most cases you'd probably want to serialize it as a string and parse it radix 10 or 16.

Personally, I'd express the type as string? Or should bigint be chosen here? (not sure if it's arbitrary precision?)

aumetra avatar Mar 20 '24 16:03 aumetra

Hey!
If we add an impl for it behind a bnum-impl feature gate, then that impl should be 1:1 compatible with their serde serialization. I think that's the only way that feature would be usefull for the average user.

If you're doing something special for serialization, you can use #[ts(type = "string")] or #[ts(as = "..")].

NyxCode avatar Mar 20 '24 16:03 NyxCode

Our impls for large integer types (e.g u128) do currently use bigint, but it was a somewhat painfull tradeoff with them. Serializing them as number would be serde-compatible by default, but probably wrong in almost all cases.

If I had the chance to go back and change it, I'd probably still chose number though. Then, it's serde-compatible, and when users chose a way of serializing them, they can also change the TS type using #[ts(type = "..")] or #[ts(as = "..")].

NyxCode avatar Mar 20 '24 16:03 NyxCode

Looking at bnum's serde feature, they just use plain derives under the hood.
They have two families of types, BIntXX and BUintXX.
BIntXX is serialized to {"bits":{"digits":[42,0,0,0,0,0,0,0]}}, BUintXX is serialized to {"digits":[42,0,0,0,0,0,0,0]}.

Therefore, it'd seem like the correct TS types would be type BIntXX = { bits: BUintXX } and type BUintXX = { digits: number[] }.

However, the "default" types I suspect most people would be using (Bint, BUint, U512, ...) use u64s as digits.
By default, that would make ts-rs create the bindings as type BUint = { digits: bigint[] }.

That would (1) make the types only correct if the data is deserialized correctly in TS (which wouldn't be the case with JSON.parse()), and (2) result in a mostly useless type.

If I'd handle these types, I'd want them treated as just a plain bigint.
I'd write a reviver to make JSON.parse() work, and enable serde_jsons arbitrary_precision feature. Finally, I'd customize serde's (de)serialization to turn a bnum type into a serde_json::Number before serializing, and deserialize a bnum type by first deserializing a serde_json::Number, and then doing the conversion.

That all takes some setup to get it working nicely.
And as it stands, bnum's serde feature is pretty useless when used with serde_json as-is.

So TL;DR: Using bnum with serde_json (which is the most common use-case for ts-rs) requires some setup, and there's no good binding we can provide for that out-of-the-box.
You should do that setup yourself, and use #[ts(as)], #[ts(type)], or a wrapper type to tell ts-rs what bindings to generate for you.

NyxCode avatar Apr 04 '25 22:04 NyxCode