npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hyperschema-ts

v0.1.1

Published

Generate TypeScript types from a Hyperschema

Readme

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-structExampleMyStruct.

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

| hyperschema | TypeScript | | ------------------------------------------------------- | ----------------------------------------------------------------------------- | | uint*, int*, float32/64, port, lexint | number | | bigint, biguint64, bigint64 | bigint | | string, utf8, ascii, hex | string | | ip, ipv4, ipv6 | string | | ipAddress, ipv4Address, ipv6Address | { host: string; family?: number; port: number } | | bool | boolean | | buffer, optionalBuffer, fixed32, fixed64, raw | Buffer | | date | Date | | none | null | | json | unknown (a json field can hold any JSON value — object, array, or scalar) | | @ns/name reference | the generated type name (e.g. NsName) |

And the modifiers:

  • array: trueT[]
  • 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 aliastype 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 registertoDisk 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:

| input | compact-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 fixed32 | throws 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:

| method | returns | narrows? | | ---------- | -------------------- | ------------------------- | | validate | { valid, errors } | no (use for error detail) | | is | value is T | yes (boolean guard) | | assert | asserts value is T | yes (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