-
Notifications
You must be signed in to change notification settings - Fork 5
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
Number objects vs. numbers #50
Comments
General answerAt least for the meantime, this is intentional yes. Since JavaScript only has a single Since JavaScript's Deeper diveSay we look at Java Edition's world save format for example, the
Number type persistenceIf each of these values were to be read into JavaScript-land as plain The base issue is that JavaScript doesn't provide enough primitive number types that will losslessly persist this intended type information down to the NBT object tree, where everything is represented in JavaScript-land. Ideally, JavaScript could have something like I also don't really like the usage of needing to interact with these import { read, write, type NBTData } from "nbtify";
import { Buffer } from "node:buffer";
// Your NBT file from the file system, network, etc.
declare const input: Uint8Array;
// NBT object tree
const nbt: NBTData = await read(input);
// Re-compiled NBT file
const output: Uint8Array = await write(nbt);
// Should return with `0`, as the bytes for these files should ideally be identical.
const identical: -1 | 0 | 1 = Buffer.compare(input, output); Alternate solution: Type inference by value rangeOne alternate take could be that inferences can be made by the current state of the value, and you could decide the primitives to use, on the fly when writing them back to the file. I really like the premise of this, and I se where it is coming from. However, this has an edgecase that makes things unsafe when a value in one of the larger value-sized types currently holds a small value, which makes the inference inaccurate depending on the current value. For the However for the This comes back to the part about possible user-interaction. If the types of the NBT file can be pre-documented to a schema of sorts, maybe this could be handled by that, and corrected when writing the file. However, this would mean that every NBT file would potentially need type documentation to write it back symmetrically. This seems too far-fetched to me. Anyone can write an NBT file, so not all files can possibly have documentation. Objects are hard to useYeah I agree, I don't like this part of it either. I wish we wouldn't have to unwrap these primitives with things like If anything, I have been wanting to see if there's a way to still use ES6 classes, and also allow for the syntax like import { Int8 } from "nbtify";
// Current
const byte0 = new Int8(25);
// Potential option, easier!
// More like `Number()` and `BigInt()` too!
const byte1 = Int8(80); Sorry this was so long, I haven't really documented this concept outright in the repo anywhere yet, so I thought I would cover all of the bases while it was relevant in the discussion. Thanks for mentioning this! |
I think I realized a possible way to make this work a bit nicer, at least from the TypeScript side of things. I can possibly expose the typings of these primitives simply as a branded type, rather than an explicit Once I get a working demo of this setup I can link it here for an example. |
This seems to be a much more useable API in terms of the TypeScript side of things. It's completely symmetrical in terms of the JavaScript implementation, so nothing changes there. It essentially just allows you to type-safely use a `Int8` for example, just as a regular `number` type. This may sound dangerous in some respects, and yes this is just a concept, so I plan to look into it to see if anything is wrong with doing it like this. Essentially, JS already converts `number` primitives back and forth for you, say like when you do `(5).toString()` for example. It's making a primitive wrapper object of that value, for a single line, and getting the result of that method call, from that object that was created under the hood. So the concept here is to use branded types to distinguish apart the number primitives, and use real `Number`-extended wrapper objects in runtime to distinguish them apart there as well. Being able to extend `Number` also allows you to pass them around and use them safely as plain `number` values too, since JS is able to do this conversion for us under the hood already. Like if you do `new Number(5) + 2`, you get `7`. TypeScript will warn you of doing this, and that has been my issue with using the wrappers for regular scripting purposes. I wanted to be able to pass them around as pure values, and not have to call `valueOf()` every single time I access their values. Another side to this though, is that this does notable break the concerns that TS provides, even though this is very much nearly doable in most JS cases. Say like you use `typeof myByte === "number"`, it will be `false` because it's actually an `object`. So maybe it makes sense to explicitly unwrap the value when you completely need to, and if not, you can still safely narrow the type and use addition for example, and decide on your own when it is safe to unwrap the type or not. I can provide more examples for this later on. I think this may be the way I take things, I really like that it allows for the use of regular function calls too, rather than with the `new` prefix. #50 https://www.youtube.com/watch?v=Yz8ySbaeCf8 https://medium.com/@apalshah/javascript-class-difference-between-es5-and-es6-classes-a37b6c90c7f8 https://esdiscuss.org/topic/extending-an-es6-class-using-es5-syntax
I think I found an actually fairly nice way to handle this! I can assert the exported types of the classes, and make them more accurate to how they are used, rather than specific to how they are implemented. Going to look into this now, it seems to look like it works very well! Using ES6 classes still is a big plus as well, as it still works with instanceof checks. TypeScript doesn't like `instanceof` calls on non-object or `never` values though, so if needed I can implement a static method maybe, like arrays? It seems to be okay with using `instanceof` on an `unknown` value, which is good. ```ts static isInt8(value: unknown): value is Int8 { return value instanceof Int8; } ``` I also really like this custom types export setup because it doesn't change the usage of existing programs, and it's only at the type level too, so things functionally won't change at all at the runtime level. Flagman, Lifesigns! #50 This commit is essentially a revert back to the start of this branch, then starting new with this custom approach.
Ok, I've merged a working version of this into the main branch, going to work with it for a bit to see if it has any issues with existing type usages. It should work nicely though! We'll see. |
Platform: Web JS, imported from https://cdn.jsdelivr.net/npm/[email protected]/+esm
When reading NBT data, integers and bytes are expressed as
Number()
objects instead of numbers. These can only be created with thenew Number()
constructor and have the following drawbacks:new Number(2) === 2
is falsetypeof new Number(2)
isobject
Is this decision intentional? I know this is true for integers and bytes, but I haven't tested other number types.
Thank you for your time and such a useful library!
The text was updated successfully, but these errors were encountered: