npm stats
  • Search
  • About
  • Repo
  • Sponsor
  • more
    • Search
    • About
    • Repo
    • Sponsor

Made by Antonio Ramirez

hyperschema-ts

0.1.0

@GitHub Actions

npmHomeRepoSnykSocket
Downloads:7768
$ npm install hyperschema-ts
DailyWeeklyMonthlyYearly

hyperschema-ts

Generate TypeScript types from a Hyperschema.

Point it at the same schema instance you pass to Hyperschema.toDisk, and it writes a .d.ts file of interface/type declarations matching your structs, enums, aliases, arrays and records. You then import those types into your app to get static typing over the objects you encode and decode.

Install

npm install hyperschema-ts

Usage

const Hyperschema = require('hyperschema')
const HyperschemaTS = require('hyperschema-ts')

const schema = Hyperschema.from('./schema')
const ns = schema.namespace('example')

ns.register({
  name: 'request',
  fields: [
    { name: 'id', type: 'uint', required: true },
    { name: 'name', type: 'string', required: true },
    { name: 'tags', type: 'string', array: true }
  ]
})

// Write the compact-encoding definitions (schema.json + index.js)
Hyperschema.toDisk(schema)

// Write the matching TypeScript declarations (types.d.ts)
HyperschemaTS.toDisk(schema)

This writes ./schema/types.d.ts:

// This file is autogenerated by hyperschema-ts
// Schema version: 1
// Do not edit manually.

export interface ExampleRequest {
  id: number
  name: string
  tags?: string[] | null
}

which you import as type-only:

import type { ExampleRequest } from './schema/types'

const req: ExampleRequest = { id: 1, name: 'a' }

On node16/nodenext resolution, import without an extension as above (a .d.ts has no runtime counterpart to point a .js specifier at). On bundler or classic resolution it resolves either way.

API

HyperschemaTS.toDisk(schema, [dir], [options])

Writes the generated declarations to disk. Mirrors Hyperschema.toDisk:

  • dir defaults to schema.dir (the directory the schema was loaded from).
  • Output file defaults to <dir>/types.d.ts.

Returns the path it wrote.

The output is pure type declarations, so it's emitted as a .d.ts. The default name avoids index.d.ts on purpose: that would sit next to hyperschema's generated index.js and TypeScript would treat it as the declarations for that module (which actually exports resolveStruct/version, not these types).

HyperschemaTS.toCode(schema, [options])

Returns the generated declarations as a string instead of writing them.

HyperschemaTS.typeName(fqn)

Converts a fully-qualified hyperschema name to its generated TS type name, e.g. @example/my-struct → ExampleMyStruct.

Options

  • filename — override the output path (otherwise <dir>/types.d.ts).
  • externals — a map of fully-qualified external type name → TS expression to substitute for it (external types default to unknown).

Type mapping

hyperschemaTypeScript
uint*, int*, float32/64, port, lexintnumber
bigint, biguint64, bigint64bigint
string, utf8, ascii, hexstring
ip, ipv4, ipv6string
ipAddress, ipv4Address, ipv6Address{ host: string; family?: number; port: number }
boolboolean
buffer, optionalBuffer, fixed32, fixed64, rawBuffer
dateDate
nonenull
jsonunknown (a json field can hold any JSON value — object, array, or scalar)
@ns/name referencethe generated type name (e.g. NsName)

And the modifiers:

  • array: true → T[]
  • a record type → Record<string, V> (records decode to a string-keyed object)
  • an enum with strings: true → a string-literal union ("a" | "b"); otherwise a numeric-literal union
  • an alias → type X = <target>

Field type overrides (tsType)

A json field carries no shape information, so it defaults to unknown. To brand a field with a concrete TS type — like Drizzle's .$type<T>() — add a tsType to the field definition. It changes only the generated type, never the encoding:

ns.register({
  name: 'doc',
  fields: [
    { name: 'metrics', type: 'json', tsType: 'Metrics' }, // → metrics?: Metrics
    { name: 'status', type: 'string', tsType: '"on" | "off"' }, // → status?: "on" | "off" | null
    { name: 'ids', type: 'string', array: true, tsType: 'Id' } // → ids?: Id[] | null
  ]
})

The override is applied as the element type, so array: true still wraps it (Id[]). You're responsible for ensuring the referenced type is in scope where the generated file is imported.

enum — narrow a string field

For a field that stays string on the wire but only holds a fixed set of values (a status/state field you can't model as a real hyperschema enum because of append-only versioning), add an enum annotation. tsType gives you the compile-time union; enum makes the validator enforce membership at runtime:

{ name: 'state', type: 'string', enum: ['queued', 'running', 'done'], tsType: '"queued" | "running" | "done"' }

Validation.validate(schema, fqn, { state: 'paused' }) then fails with state: must be one of "queued", "running", "done". For an array field the check applies element-wise. (A genuine hyperschema enum type is still preferable when you can use one — it also encodes as a 1-byte index and is rejected by compact-encoding itself.)

Both tsType and enum are read from the live schema instance. Hyperschema does not persist unknown field keys to schema.json, so annotations only take effect when you generate types / validate in the same process that registers the schema (the usual register → toDisk build script). They are lost across a Hyperschema.from('./dir') reload.

Optional fields and decode semantics

Hyperschema's non-required fields don't decode to undefined — they decode to the type's default. So the generated types reflect both sides:

  • Every optional field is marked ? (for the encode side).
  • Optional fields whose default is null (objects, strings, buffers, arrays) are also unioned with | null (for the decode side).
  • Optional scalar fields keep their plain type, since their default is a real value (0, 0n, false), not null.
export interface Example {
  id: number // required
  score?: number // optional scalar → decodes to 0
  blob?: Buffer | null // optional buffer → decodes to null
  tags?: string[] | null // optional array → decodes to null
}

Note: bool and bitwise uint fields are always optional, because hyperschema stores them in the struct's flags bitfield.

Validation

hyperschema-ts/validation validates plain objects against a schema type before you hand them to compact-encoding.

This matters because compact-encoding is mostly silently lenient — it only throws in a couple of cases and otherwise corrupts bad input without complaint:

inputcompact-encoding
ipv4("hello")silently encodes 143.0.0.0
uint8(9999)silently truncates to 15
hex("zz")silently encodes 00
uint(-5)throws uint must be positive
wrong-size fixed32throws Incorrect buffer size

The validator rejects everything compact-encoding would throw on or silently corrupt (out-of-range numbers, malformed IPs, wrong buffer sizes, bad enum values, missing required fields, …), with a path for each failure.

const Validation = require('hyperschema-ts/validation')

const { valid, errors } = Validation.validate(schema, '@example/request', {
  id: 1,
  host: 'hello' // not a valid IPv4
})
// valid === false
// errors === [{ path: '@example/request.host', value: 'hello', message: 'must be a valid IPv4 address' }]

// or throw an aggregated error:
Validation.assert(schema, '@example/request', obj)

Validation.validate(schema, fqn, value)

Returns { valid, errors }, where each error is { path, value, message }. Never throws (except on an unknown fqn).

Validation.assert(schema, fqn, value)

Throws an Error listing every failure if value is invalid; otherwise returns nothing.

Typed validation (createValidator)

Every generated types.d.ts also exports a SchemaTypes registry mapping each fqn to its type. Pass it to createValidator and the validator becomes type-safe: fqn autocompletes (and typos fail to compile), and is/assert narrow value to the matching type.

import HyperschemaValidation from 'hyperschema-ts/validation'
import type { SchemaTypes } from './schema/types'

const v = HyperschemaValidation.createValidator<SchemaTypes>(schema)

if (v.is('@example/request', value)) {
  value.id // value is ExampleRequest here
}

v.assert('@example/request', value) // throws, or narrows value to ExampleRequest
const { valid, errors } = v.validate('@example/request', value)

A value is T guard must return a boolean, so the three forms split by intent:

methodreturnsnarrows?
validate{ valid, errors }no (use for error detail)
isvalue is Tyes (boolean guard)
assertasserts value is Tyes (throws on failure)

createValidator needs a live schema at runtime — typically Hyperschema.from('./schema') at startup. Note that a reload-from-disk schema loses field-level tsType/enum annotations (hyperschema doesn't persist them), so annotation-based enum membership only fires when validating against the same instance that registered the schema.

Records are validated by value; their keys are always JS strings (compact-encoding records key on Object.keys). External and versioned types are treated as opaque (not validated structurally).

License

Apache-2.0