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

@voxgig/struct

v0.2.1

Published

JSON data structure manipulations

Readme

Struct for TypeScript

The canonical implementation. Every other language port matches the behaviour defined here. When the shared test corpus (../build/test/) and another port disagree, the corpus -- generated from this code -- is right.

For motivation, language-neutral concepts, and the cross-language parity matrix, see the top-level README and REPORT.md.

Install

npm install @voxgig/struct

Package: @voxgig/struct. Entry: dist/StructUtility.js. Types: dist/StructUtility.d.ts. Module type: CommonJS.

Quick start

import {
  getpath, setpath, merge, walk,
  inject, transform, validate, select,
} from '@voxgig/struct'

// Read a deep value.
getpath({ db: { host: 'localhost' } }, 'db.host')
// => 'localhost'

// Build by example: spec mirrors the desired output, with backtick
// references into the data.
transform(
  { user: { first: 'Ada', last: 'Lovelace' }, age: 36 },
  { name: '`user.first`', surname: '`user.last`', years: '`age`' }
)
// => { name: 'Ada', surname: 'Lovelace', years: 36 }

// Check the shape of incoming data.
validate(
  { name: 'Ada', age: 36 },
  { name: '`$STRING`', age: '`$INTEGER`' }
)
// => { name: 'Ada', age: 36 }   (throws on mismatch)

Imports

import {
  // 29 minor utilities
  typename, getdef, isnode, ismap, islist, iskey, isempty, isfunc,
  size, slice, pad, typify, getelem, getprop, strkey, keysof,
  haskey, items, flatten, filter, escre, escurl, join, jsonify,
  stringify, pathify, clone, delprop, setprop,

  // 8 major utilities
  walk, merge, setpath, getpath, inject, transform, validate, select,

  // 2 builders
  jm, jt,

  // 3 injection helpers
  checkPlacement, injectorArgs, injectChild,

  // sentinels
  SKIP, DELETE,

  // 15 type bit-flags
  T_any, T_noval, T_boolean, T_decimal, T_integer, T_number, T_string,
  T_function, T_symbol, T_null, T_list, T_map, T_instance, T_scalar,
  T_node,

  // walk/inject mode flags
  M_KEYPRE, M_KEYPOST, M_VAL, MODENAME,

  // class wrapper
  StructUtility,

  // type-only
  Injection,
} from '@voxgig/struct'

Function reference

Every function is documented with its TypeScript signature, a one-line description, and a usage example. Source: src/StructUtility.ts.

Predicates

function isnode(val: any): val is Indexable
function ismap(val: any): val is { [key: string]: any }
function islist(val: any): val is any[]
function iskey(key: any): key is string | number
function isempty(val: any): boolean
function isfunc(val: any): val is Function
isnode({ a: 1 })          // true
isnode([1, 2])            // true
isnode('a')               // false
ismap({ a: 1 })           // true
ismap([1])                // false
islist([1, 2])            // true
islist({ a: 1 })          // false
iskey('name')             // true
iskey(0)                  // true
iskey('')                 // false
iskey(true)               // false
isempty([])               // true
isempty(null)             // true
isempty('')               // true
isempty({})               // true
isempty(0)                // false

isfunc(() => 1)           // true
isfunc('foo')             // false

Type inspection

function typify(value: any): number
function typename(t: number): string

typify returns a bit-field combining a "kind" flag (T_scalar or T_node) with a specific type flag. typename looks up a human-friendly name.

typify(1)                 // T_scalar | T_number | T_integer  (201326720)
typify(42)                // T_scalar | T_number | T_integer
typify('hi')              // T_scalar | T_string
typify(null)              // T_scalar | T_null
typify(undefined)         // T_noval
typify({})                // T_node | T_map
typify([])                // T_node | T_list
typename(8192)            // "map"  (8192 === T_map)
typename(typify(42))      // "integer"
typename(typify('hi'))    // "string"
typename(typify({}))      // "map"

Size, slice, pad

function size(val: any): number
function slice<V>(val: V, start?: number, end?: number, mutate?: boolean): V
function pad(str: any, padding?: number, padchar?: string): string
size([1, 2, 3])           // 3
size({ a: 1, b: 2 })      // 2
size('abc')               // 3
size(7.9)                 // 7

slice keeps the first N; a negative start drops the last |start| items, and end is exclusive:

slice([1, 2, 3, 4, 5], 1, 4)   // [2, 3, 4]
slice('abcdef', -3)            // 'abc'  (drops the last 3)
slice(10, 0, 5)                // 4  (number bounding; end is exclusive)
pad('a', 3)               // 'a  '
pad('hi', 5)              // 'hi   '
pad('hi', -5)             // '   hi'
pad('hi', 5, '*')         // 'hi***'

Property access

function getprop(val: any, key: any, alt?: any): any
function setprop<P>(parent: P, key: any, val: any): P
function delprop<P>(parent: P, key: any): P
function getelem(val: any, key: any, alt?: any): any
function getdef(val: any, alt: any): any
function haskey(val: any, key: any): boolean
function keysof(val: any): string[]
function items(val: any): [string, any][]
function items<T>(val: any, apply: (item: [string, any]) => T): T[]
function strkey(key?: any): string
getprop({ x: 1 }, 'x')               // 1
getprop({ a: 1 }, 'b', 'default')    // 'default'
getprop([10, 20, 30], 1)             // 20
setprop({ a: 1 }, 'b', 2)            // { a: 1, b: 2 }
setprop([1, 2, 3], 0, 9)             // [9, 2, 3]
delprop({ a: 1, b: 2 }, 'a')         // { b: 2 }
getelem([10, 20, 30], -1)            // 30
getelem([1, 2, 3], 5, 'none')        // 'none'

getdef(undefined, 'fallback')        // 'fallback'
getdef('value', 'fallback')          // 'value'
haskey({ a: 1 }, 'a')                // true
haskey({ a: undefined }, 'a')        // false

keysof([10, 20, 30])                 // ['0', '1', '2']
keysof({ b: 4, a: 5 })               // ['a', 'b']  (sorted)
items({ a: 1, b: 2 })                // [['a', 1], ['b', 2]]
items([10, 20])                      // [['0', 10], ['1', 20]]
items({ a: 1 }, ([k, v]) => `${k}=${v}`)
                                     // ['a=1']
strkey(2.2)                          // '2'
strkey(1)                            // '1'
strkey('foo')                        // 'foo'
strkey(true)                         // ''  (invalid keys -> '')

Path operations

function getpath(store: any, path: number | string | string[], injdef?: Partial<Injection>): any
function setpath(store: any, path: number | string | string[], val: any, injdef?: Partial<Injection>): any
function pathify(val: any, startin?: number, endin?: number): string
getpath({ a: { b: { c: 42 } } }, 'a.b.c')       // 42
getpath({ a: { b: { c: 42 } } }, ['a', 'b', 'c'])
getpath({ a: [10, 20] }, 'a.1')                 // 20
getpath({ a: 1 }, 'missing')                    // undefined

const store = {}
setpath(store, 'db.host', 'localhost')
// store === { db: { host: 'localhost' } }

setpath({ a: [1, 2, 3] }, 'a.1', 99)
// { a: [1, 99, 3] }
setpath({ a: 1, b: 2 }, 'b', 22)                // { a: 1, b: 22 }
pathify(['a', 'b', 'c'])                        // 'a.b.c'
pathify('a.b.c')                                // 'abc'  (a plain string is not split on dots)
pathify(['a', 'b', 'c'], 1)                     // 'b.c'

Tree operations

function walk(
  val: any,
  before?: WalkApply,
  after?: WalkApply,
  maxdepth?: number,
  // recursive state args:
  key?, parent?, path?, pool?
): any

function merge(val: any[], maxdepth?: number): any
function clone(val: any): any
function flatten(list: any[], depth?: number): any[]
function filter(val: any, check: (item: [string, any]) => boolean): any[]

type WalkApply = (
  key: string | number | undefined,
  val: any,
  parent: any,
  path: string[]
) => any
// Replace nulls with 'DEFAULT' on ascend.
walk(tree, undefined, (key, val, parent, path) =>
  val === null ? 'DEFAULT' : val
)

Last input wins; maps deep-merge; lists merge by index:

merge([
  { a: 1, b: 2, k: [10, 20], x: { y: 5, z: 6 } },
  { b: 3, d: 4, e: 8, k: [11], x: { y: 7 } },
])
// { a: 1, b: 3, d: 4, e: 8, k: [11, 20], x: { y: 7, z: 6 } }
clone({ a: { b: [1, 2] } })             // { a: { b: [1, 2] } }  (a deep copy)
flatten([1, [2, [3]]])                  // [1, 2, [3]]  (one level by default)
flatten([1, [2, [3, [4]]]])             // [1, 2, [3, [4]]]
flatten([1, [2, [3, [4]]]], 2)          // [1, 2, 3, [4]]

filter passes each [key, value] pair to the check and returns the matching values (not the pairs):

filter([1, 2, 3, 4, 5], ([k, v]) => v > 3)
// [4, 5]

String / URL / JSON

function escre(s: string): string
function escurl(s: string): string
function join(arr: any[], sep?: string, url?: boolean): string
function jsonify(val: any, flags?: { indent?: number, offset?: number }): string
function stringify(val: any, maxlen?: number, pretty?: any): string
escre('a.b+c')                          // 'a\\.b\\+c'
escurl('hello world?')                  // 'hello%20world%3F'
join(['a', 'b', 'c'], '/')              // 'a/b/c'
join(['http:', '/foo/', '/bar'], '/', true)
                                        // 'http:/foo/bar'  (URL-mode collapses)

jsonify pretty-prints by default (indent 2); pass { indent: 0 } for the compact form:

jsonify({ a: 1 })
// {
//   "a": 1
// }
jsonify({ a: 1, b: 2 }, { indent: 0 })  // '{"a":1,"b":2}'

stringify is the compact, quote-light form — keys are sorted and object braces are kept; the second argument caps the length (the ... counts):

stringify({ a: 1, b: [2, 3] })          // '{a:1,b:[2,3]}'
stringify('verylongstring', 5)          // 've...'

Injection / transform / validate / select

function inject(
  val: any,
  store: any,
  injdef?: Partial<Injection>,
): any

function transform(
  data: any,         // source data
  spec: any,         // by-example output shape
  injdef?: Partial<Injection>
): any

function validate(
  data: any,         // input to check
  spec: any,         // expected shape with checker tokens
  injdef?: Partial<Injection>
): any

function select(children: any, query: any): any[]
// Backtick refs in strings are replaced by store values.
inject({ x: '`a`', y: 2 }, { a: 1 })    // { x: 1, y: 2 }
inject(
  { greeting: 'hello `name`', age: '`years`' },
  { name: 'Ada', years: 36 }
)
// { greeting: 'hello Ada', age: 36 }

// Build a result by example.
transform(
  { hold: { x: 1 }, top: 99 },
  { a: '`hold.x`', b: '`top`' }
)
// { a: 1, b: 99 }

Transform commands drive structural ops. A command like $EACH appears in value position — as the first element of a list ['$EACH', path, subspec] — mapping the sub-spec over every entry at path:

transform(
  { v: 1, a: [{ q: 13 }, { q: 23 }] },
  { x: { y: ['`$EACH`', 'a', { q: '`$COPY`', r: '`.q`', p: '`...v`' }] } }
)
// { x: { y: [ { q: 13, r: 13, p: 1 }, { q: 23, r: 23, p: 1 } ] } }

Putting a command in key position (or, for $APPLY, directly under a map) is an error — commands must be list values:

transform({}, { x: '`$APPLY`' })
// throws: $APPLY: invalid placement in parent map, expected: list.
// Validate against a shape (throws on mismatch).
validate(
  { name: 'Ada', age: 36 },
  { name: '`$STRING`', age: '`$INTEGER`' }
)
// { name: 'Ada', age: 36 }
// Find children matching a query.
select(
  { a: { name: 'Alice', age: 30 }, b: { name: 'Bob', age: 25 } },
  { age: 30 }
)
// [{ name: 'Alice', age: 30, $KEY: 'a' }]

Builders

function jm(...kv: any[]): Record<string, any>
function jt(...v: any[]): any[]
jm('a', 1, 'b', 2)        // { a: 1, b: 2 }
jt(1, 2, 3)               // [1, 2, 3]

Useful for building literal-looking JSON shapes inside expressions.

Injection helpers

These are exposed for callers writing custom injectors or modify hooks; most users will not need them directly.

function checkPlacement(
  modes: number,            // bitmask of M_KEYPRE | M_KEYPOST | M_VAL
  ijname: string,           // injection name (transform/validate command)
  parentTypes: number,      // expected parent types
  inj: Injection
): boolean

function injectorArgs(argTypes: number[], args: any[]): any
function injectChild(child: any, store: any, inj: Injection): Injection

Constants

Sentinels

SKIP        // emit nothing for this key
DELETE      // remove this key from the parent

Returned from a transform / inject step to signal these structural operations to the caller.

Type bit-flags

typify(val) returns a bit-field combining a kind flag with a specific type:

T_any         // wildcard / no constraint
T_noval       // property absent / undefined (NOT a scalar)
T_boolean     // boolean scalar
T_decimal     // non-integer numeric scalar
T_integer     // integer numeric scalar
T_number      // any number (set together with T_integer/T_decimal)
T_string      // string scalar
T_function    // function value
T_symbol      // symbolic atom (for languages that have them)
T_null        // JSON null (distinct from absent)
T_list        // list node (array)
T_map         // map node (object)
T_instance    // class instance (non-plain object)
T_scalar      // bit set on every scalar type
T_node        // bit set on every node type
const t = typify('hi')
0 < (T_string & t)        // true
0 < (T_scalar & t)        // true
typename(t)               // 'string'

Walk / inject phase flags

M_KEYPRE      // about to descend into a child by key
M_KEYPOST     // returned from descending into a child by key
M_VAL         // visiting the value of a leaf
MODENAME      // string[] — human names by mode flag

Transform commands

Quote inside a transform spec, e.g. '`$COPY`'.

| Command | Purpose | |-----------|-------------------------------------------------------------------| | $DELETE | Remove the current key from the output. | | $COPY | Copy the matching value from data at the current path. | | $KEY | Emit the current key under another name. | | $ANNO | Annotate the current node with extra fields. | | $MERGE | Deep-merge several sub-specs into the current node. | | $EACH | Apply a sub-spec to every entry of a list or map. | | $PACK | Repack a node by rewriting its keys / shape. | | $REF | Resolve a named reference inside the spec. | | $FORMAT | Render a templated string using values from data. | | $APPLY | Call a function (from extra / injdef) on the current value. | | $DS | Emit a literal $ (escape the dollar sign). | | $WHEN | Insert the current date and time as an ISO-8601 string. |

Validate checkers

Quote inside a validate spec, e.g. '`$STRING`'.

| Checker | Accepts | |-------------|-----------------------------------------------------------------| | $MAP | A map. | | $LIST | A list. | | $STRING | A string. | | $NUMBER | A number (integer or decimal). | | $INTEGER | An integer. | | $DECIMAL | A non-integer number. | | $BOOLEAN | A boolean. | | $NULL | JSON null. | | $NIL | Null or absent (lenient). | | $FUNCTION | A callable function. | | $INSTANCE | A class instance. | | $ANY | Any value (placeholder for "no constraint"). | | $CHILD | Apply a sub-spec to every direct child. | | $ONE | Match exactly one of a list of alternative sub-specs. | | $EXACT | Match a literal value exactly (no shape coercion). |

StructUtility class

Wraps every function and constant as instance properties. Useful when you want to inject the API surface (e.g. for stubbing in tests):

import { StructUtility } from '@voxgig/struct'

const su = new StructUtility()
su.getpath({ a: { b: 1 } }, 'a.b')      // 1
su.merge([{ a: 1 }, { b: 2 }])          // { a: 1, b: 2 }

Notes

null versus undefined

TypeScript distinguishes the two; struct preserves the distinction:

  • undefined means "absent". getprop returns undefined for a missing key, and most predicates treat undefined as not-a-node.
  • null is the JSON null value -- a defined scalar. typify(null) returns T_scalar | T_null.

Most JSON parsers conflate them; if your data source returns null for absent fields, convert them before passing into struct -- or adopt the '__NULL__' placeholder used by the shared test corpus.

Lists are mutable and reference-stable

walk, merge, inject, and setpath all rely on lists being reference-stable: a mutation through one reference is visible to every holder. This is JavaScript's natural behaviour; no wrapper needed.

Path syntax

Paths can be:

  • a dot-separated string: 'a.b.0.c'
  • an array: ['a', 'b', 0, 'c']

Integer-looking string keys index into lists; everything else indexes maps.

walk paths are reused

The path array passed to a WalkApply callback is reused across calls (one shared array per depth). Clone it (path.slice()) if you need to retain it past the callback.

Regex

The library exposes a uniform six-function regex API across every port (see /design/REGEX_API.md for the contract and /design/REGEX.md for the supported dialect). On TypeScript the canonical implementation is ECMAScript RegExp.

API

| Function | Maps to | |---|---| | re_compile(pattern, flags?) | new RegExp(pattern, flags ?? 'g') | | re_test(pattern, input) | pattern.test(input) | | re_find(pattern, input) | input.match(pattern) (non-global pattern) | | re_find_all(pattern, input) | [...input.matchAll(pattern)] | | re_replace(pattern, input, rep) | input.replace(pattern, rep) (global pattern) | | re_escape(s) | escape [.*+?^${}()|[\]\\] in s |

Dialect

Patterns must stay inside the RE2 subset documented in /design/REGEX.md: literals + escapes, ., ^/$, * + ? {n} {n,} {n,m} (greedy + lazy), character classes incl. \d \w \s etc., \b/\B, (...) / (?:...) / (?<name>...), alternation. ECMAScript RegExp supports backreferences and lookaround, but other ports do not — using those will not be portable.

Sharp edges

  • Catastrophic backtracking. ECMAScript RegExp uses backtracking; nested quantifiers (e.g. (a+)+) against a non-matching suffix can be exponential in the input length. The discovery panel measures ~180 ms on Node 22 for ^(a+)+$ against 22 a's plus !. RE2-style engines finish the same case in under 0.1 ms. Write linear-friendly patterns (a+ instead of (a+)+) and keep injected user input in character classes, not in alternations.
  • Zero-width replace. re_replace("a*", "abc", "X") returns "XXbXcX" here — the ECMA convention shared by every port whose host engine is PCRE/ECMA/.NET/Java/Onigmo, plus the in-tree Thompson NFA ports (Rust / C / Lua / Zig). Go is the exception: RE2 returns "XbXcX". Don't rely on cross-port identity of zero-width replacement output — see /design/REGEX_PATHOLOGICAL.md.

See /design/REGEX_PATHOLOGICAL.md for the cross-port pathological-input panel and per-port outcomes.

Build and test

cd typescript
npm install
npm run build              # tsc -> dist/
npm test                   # node --test on dist-test/

Tests live in test/ and read fixtures from ../build/test/. The test runner consumes the shared JSONIC corpus shared by every language port.