The point of this package is provide the ability to manipulate binary structured data in a typesafe, declarative, object-oriented way.
- Read and write binary-structured data with the same declaration
- Single source of truth - changes to logical view are immediately reflected in the underlying binary structure and vice versa.
- Written in TypeScript, written FOR JavaScript. Declaring a struct allows full type inference; no separate type declaration required!
- Clean object format. No risk of your struct field names colliding with implementation details. Type inference won't show excessive properties.
This example is pure JavaScript, but note that all property access is fully typechecked.
import {
defineArray,
defineStruct,
f32,
string,
substruct,
u8,
} from "@rotu/structview"
// defineStruct returns a new class, which can be extended with custom getters, setters, and methods.
// It's recommended to always extend even if you have no additional members, so your class has a name and so the declaration is hoisted.
class Version extends defineStruct({
major: u8(0),
minor: u8(1),
patch: u8(2),
}) {
asString() {
// Struct fields are exposed as properties. They can be destructured like any other js object
const { major, minor, patch } = this
return `${major}.${minor}.${patch}`
}
}
class ProductInfo extends defineStruct({
version: substruct(Version, 0, 3),
product: string(4, 12),
}) {}
const bytes = new Uint8Array(16)
const info = new ProductInfo(bytes)
info.product = "Widget"
// Basic object assignment and destructuring just works
Object.assign(info.version, { major: 1, minor: 42, patch: 1 })
// Object writes are saved in the underlying buffer
console.log(
"encoded hex:",
bytes.toHex(),
)
console.log(`${info.product} v${info.version.asString()}`)
// You can compose structs into arrays
class Dish extends defineStruct({
price: f32(0),
name: string(4, 12),
}) {}
class Menu extends defineArray({
struct: Dish,
byteStride: 16,
length: 3,
}) {}
const myMenu = Menu.alloc({ byteLength: 48 })
Object.assign(myMenu.item(0), { name: "garden salad", price: 4 })
Object.assign(myMenu.item(1), { name: "soup du jour", price: 2.5 })
Object.assign(myMenu.item(2), { name: "fries", price: 2.25 })
// and arrays are iterable
for (const dish of myMenu) {
console.log(`${dish.name} costs \$${dish.price}`)
}Document reserved or padding regions without occupying a meaningful field name.
The property is non-enumerable, so it is excluded from JSON.stringify and
structToObject.
import { defineStruct, pad, u8, u32 } from "@rotu/structview"
class Header extends defineStruct({
version: u8(0),
_reserved: pad(1, 3), // 3 reserved bytes
length: u32(4),
}) {
static BYTE_LENGTH = 8
}Map a raw integer field to human-readable string labels. When reading, the
integer is looked up in values; unknown integers are returned as-is. When
writing, you may pass a label string or the raw integer.
Inspired by enum types in kaitai-struct and restructure.
import { defineStruct, enumField, u8 } from "@rotu/structview"
const STATUS = { 0: "idle", 1: "busy", 2: "error" }
class Packet extends defineStruct({
status: enumField(u8(0), STATUS),
data: u32(4),
}) {
static BYTE_LENGTH = 8
}
const p = Packet.alloc()
p.status = "busy"
console.log(p.status) // "busy"Convert any struct to a plain Record<string, unknown> by iterating all
enumerable (prototype-defined) fields. Useful for cloning, diffing, or any
place that needs a plain JS object.
import { defineStruct, f32, structToObject } from "@rotu/structview"
class Vec2 extends defineStruct({ x: f32(0), y: f32(4) }) {}
const v = Vec2.alloc({ byteLength: 8 })
v.x = 3; v.y = 4
const plain = structToObject(v) // { x: 3, y: 4 }
console.log(JSON.stringify(plain)) // '{"x":3,"y":4}'- Resizable structs are not yet implemented. Resizable
Arraybuffers only allow you to add or remove bytes at the end which is not the best experience. You can still create aStructon top of a resizableArrayBufferat your own risk. - Struct fields have a byte offset specified in bytes from the beginning of the
declaring struct. This is a bit verbose but is a deliberate choice.
- It prevents changes to the struct from accidentally changing other fields
- It implicitly allows C-style
unions for free. - Different languages and compilers have different expectations for alignment and spacing of fields.
- Be careful using
TypedArrays. They have an alignment requirement relative to their underlyingArrayBuffer. Structclasses define properties on the prototype, not on the instance. That means spread syntax (x = {...s}) will not reflect inherited fields. However,JSON.stringify(s)does now work becauseStructimplementstoJSON(). For spread-like use cases, use thestructToObject(s)helper.