Skip to content

Custom Schema Types

Custom schema types can be defined. A schema is any object that implements the bin.Schema<TIn, TOut> interface, where TIn is the type of values it accepts when writing, and TOut the type of values it produces when reading. TOut defaults to TIn, so bin.Schema<number> reads and writes numbers.

import * as bin from 'typed-binary';
/**
* A schema storing radians with 2 bytes of precision.
*/
class RadiansSchema implements bin.Schema<number> {
read(cursor: bin.ByteCursor): number {
const low = bin.u8.read(cursor);
const high = bin.u8.read(cursor);
const discrete = (high << 8) | low;
return (discrete / 65535) * Math.PI;
}
write(cursor: bin.ByteCursor, value: number): void {
// The value will be wrapped to be in range of [0, Math.PI)
const wrapped = ((value % Math.PI) + Math.PI) % Math.PI;
// Quantizing the value to range of [0, 65535]
const discrete = Math.min(Math.floor((wrapped / Math.PI) * 65535), 65535);
const low = discrete & 0xff;
const high = (discrete >> 8) & 0xff;
bin.u8.write(cursor, low);
bin.u8.write(cursor, high);
}
measure(_: number | bin.MaxValue): number {
// The size of the data serialized by this schema
// doesn't depend on the actual value. It's always 2 bytes.
return 2;
}
}
// Creating a singleton instance of the schema,
// since it has no configuration properties.
export const radians = new RadiansSchema();
// Custom schemas can be used like any other schema
const Rotation = bin.struct({ roll: radians, pitch: radians, yaw: radians });

Schemas read and write through a bin.ByteCursor, which is a DataView along with the current byte offset and the endianness to use. Custom schemas can delegate to the primitive schemas, like the bin.u8 calls above, or use the DataView directly:

read(cursor: bin.ByteCursor): number {
const value = cursor.view.getUint16(cursor.offset, cursor.littleEndian);
cursor.offset += 2;
return value;
}

When measuring with bin.MaxValue, a schema should return the biggest size its values can take. If there is no upper bound (e.g. for variable-length data), it should return NaN.

Sometimes the thing you want to encode is already described by an object of your own, like a component definition, a message type or a class. Instead of keeping a separate schema next to it, the object can define itself as a schema by holding one under the '~typed-binary' key:

import * as bin from 'typed-binary';
function component<TSchema extends bin.Schema>(name: string, schema: TSchema) {
return { name, '~typed-binary': { schema } } as const;
}
const Health = component('health', bin.u16);
const Position = component('position', bin.struct({ x: bin.f32, y: bin.f32 }));
// The components can be used anywhere a schema is expected
const Entity = bin.struct({
id: bin.u32,
health: Health,
position: Position,
});
type Entity = bin.ExtractOut<typeof Entity>;
// ^? { id: number, health: number, position: { x: number, y: number } }

Such objects are accepted by every schema constructor: as struct properties, elements of bin.array, bin.dynamicArray and bin.tuple, the inner schema of bin.optional, the structs passed to bin.concat, the tag, variants and properties of bin.union, and the head of bin.pipe (or the schema returned by its action). bin.ExtractIn and bin.ExtractOut work on them as well, and the bin.WithSchema<TIn, TOut> type describes either a schema or an object defining itself as one.

The object itself is not a schema, so reading and writing is done through the schema it holds, or through a schema that contains it:

const buffer = new ArrayBuffer(Entity.maxSize);
Entity.write(bin.cursor(buffer), {
id: 1,
health: 100,
position: { x: 0.5, y: 0.25 },
});

Classes can define themselves as schemas with a static property:

class Vec2 {
static readonly '~typed-binary' = {
schema: bin.struct({ x: bin.f32, y: bin.f32 }),
};
}
const Transform = bin.struct({ position: Vec2, scale: Vec2 });

To reference a schema that is defined later (or the object’s own schema), define '~typed-binary' as a getter, and reference the object from inside a getter of a struct property:

const TreeNode = {
kind: 'tree-node',
get '~typed-binary'() {
return { schema: TreeNodeSchema };
},
};
const TreeNodeSchema = bin.struct({
value: bin.i32,
get children() {
return bin.dynamicArray(TreeNode);
},
});
type TreeNode = bin.ExtractOut<typeof TreeNodeSchema>;
// ^? { value: number, children: TreeNode[] }