mrkeyoor.com_
Wed 23 Sept 00:32 UTC
npmUtilsupdated 22 Sept 2026

superstruct review

Superstruct 2.0.2 validates unknown JavaScript values at runtime with composable structs for objects, arrays, records, unions, primitives, coercions, and custom refinements. Callers can throw with `assert`, narrow with the Boolean `is` guard, receive an error tuple from `validate`, transform with `create`, or remove unknown object keys with `mask`. `StructError` carries the value, key, path, branch, refinement, and an iterator over every failure. Version 2.0.2 fixes coercion of frozen objects and arrays. Our browser build measured 10.5 KB minified and 3.5 KB gzipped with no package dependencies.

Verdict

Superstruct 2.0.2 added 3.5 KB gzipped in our browser build and installed in 0.6 seconds with 0 dependencies and 0 audit findings, making its runtime cost easy to accept. Its October 2024 last push is the harder tradeoff, so new long-lived TypeScript systems should compare Zod or Valibot before committing.

We installed it

Lab card: what happened when we installed superstructScreenshot of superstruct documentation
Install✓ · 0.6s1 package on disk · 1 MB
ImportESM import works · require() works · ESM package
Browser3.5 KBgzipped (10.5 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does superstruct install cleanly?

Yes. In a fresh container with an empty cache, npm install superstruct finished in 0.6s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does superstruct add to a browser bundle?

3.5 KB gzipped (10.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does superstruct work with both ESM and CommonJS?

Yes. Both import 'superstruct' and require('superstruct') worked in Node 22 in our run. The package is published as ESM.

Does superstruct include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

superstruct or zod: which should you use?

zod: Use Zod when ecosystem breadth, frequent releases, and TypeScript-first integrations outweigh package size. Superstruct 2.0.2 added 3.5 KB gzipped in our browser build and installed in 0.6 seconds with 0 dependencies and 0 audit findings, making its runtime cost easy to accept.

When should you not use superstruct?

A new platform needs visibly active maintenance. Version 2.0.2 was published in July 2024 and the repository's last push was October 2024.

API stability4/5Superstruct 2.0.2 keeps the version 2 operations and factories unchanged: `assert`, `is`, `validate`, `create`, `mask`, object types, coercions, and refinements retain their established roles. The current patch only permits coercion of frozen arrays and objects. Zero dependencies also remove transitive behavior drift. The missing point reflects the other side of that calm: with no source push since October 2024, future TypeScript and runtime adaptation is uncertain.
Docs4/5The official site separates validation, coercion, refinement, errors, and TypeScript into focused guides, then documents core operations, types, utilities, coercions, and `StructError` in references. It explains strict object keys, permissive `type`, `mask`, `strictNullChecks`, invalid Date handling, and when defaults execute. Some pages and examples predate the current TypeScript ecosystem, but the important behavioral distinctions are stated directly.
Maintenance2/5Version 2.0.2 was published on July 6, 2024, and the repository's last recorded push was October 1, 2024. GitHub shows 7,135 stars, an unarchived repository, and 82 open issues after pull requests are excluded. The release fixed frozen-value coercion, but there has been no later package or source activity visible in the fetched metadata. Existing pinned schemas may stay stable; new compatibility work should not be assumed.
Ecosystem3/5The npm endpoint counted 6,175,697 downloads for August 18 through August 24, 2026. Our install found bundled types, working CommonJS and ESM loading, 0 dependencies, and a 3.5 KB gzipped browser build. Superstruct fits any JavaScript framework through plain values and detailed errors, but its form adapters, code generators, OpenAPI bridges, and current community examples are materially thinner than Zod's surrounding ecosystem.

Use it if

  • One schema should support throwing checks, Boolean type guards, typed tuple validation, and deliberate coercion.
  • Application-specific identifiers and strings need small named predicates instead of a large built-in format catalog.
  • Unknown object keys should either fail under `object`, remain under `type`, or be removed through `mask`.
  • Detailed paths and all failures must be translated into your API's own error response.
Skip it if

Setup reality

We installed superstruct 2.0.2 in a fresh Node 22 Bookworm sandbox. npm completed in 0.6 seconds, left 1 package using 1 MB, and found 0 known vulnerabilities. The package has 0 direct and 0 peer dependencies, bundled TypeScript declarations, a 240 KB unpacked size, and a Node floor of 14. It declares ESM without an exports map; require() and ESM import both worked. Our browser import was 10.5 KB minified and 3.5 KB gzipped.

There is no plugin or config file. Schema design carries the setup burden. object() rejects extra keys, type() accepts them, and mask() removes extras when the struct supports masking. Choose that boundary before validating API data, because a backend adding one field can break a strict consumer while a permissive struct can pass data you never reviewed.

Validation and transformation are separate. assert() throws and narrows, is() returns a type guard, and validate() returns [error, value]. Defaults and custom coercions run through create(), mask(), or validate(input, struct, { coerce: true }); they do not run through assert() or is(). Version 2.0.2 specifically fixes coercing frozen arrays and objects.

Enable TypeScript strict or at least strictNullChecks; optional() and nullable() represent different inputs. Convert StructError.failures() into stable public error codes rather than exposing library messages as an API contract. number() accepts Infinity, regexp() validates a RegExp object, and recursive lazy() schemas usually need an explicit TypeScript type. Pin 2.0.2 and test these semantics because upstream has been quiet since 2024.

Patterns

Throw on an invalid object assert-object

import { array, assert, number, object, string } from 'superstruct'

const Article = object({
  id: number(),
  title: string(),
  tags: array(string()),
})
assert(payload, Article)
console.log(payload.title)

`object()` rejects unknown properties. A successful `assert()` narrows the TypeScript value and leaves it uncoerced.

Narrow without throwing boolean-guard

import { is, number, object, string } from 'superstruct'

const User = object({ id: number(), name: string() })
if (is(input, User)) {
  console.log(input.name)
}

`is()` returns a Boolean type guard. It does not apply defaults or any other coercion.

Map every failure to an API shape collect-failures

import { validate } from 'superstruct'

const [error, value] = validate(input, User)
if (error) {
  return [...error.failures()].map(failure => ({
    path: failure.path.join('.'),
    message: failure.message,
  }))
}
return value

The tuple contains either a `StructError` or the typed value. `failures()` iterates every discovered problem, not just the first.

Create data with a default apply-default

import { create, defaulted, number, object, string } from 'superstruct'

const Search = object({
  query: string(),
  limit: defaulted(number(), 20),
})
const search = create({ query: 'printer' }, Search)

Defaults execute through `create()`. Running `assert()` or `is()` on the same missing `limit` does not insert 20.

Return a masked public object strip-extra-keys

import { mask, number, object, string } from 'superstruct'

const PublicUser = object({ id: number(), name: string() })
const clean = mask({ id: 1, name: 'Ada', admin: true }, PublicUser)

`mask()` with `object()` removes `admin`. A `type()` struct permits and retains extra properties instead.

Validate named fields while retaining extras allow-extra-keys

import { number, string, type } from 'superstruct'

const Event = type({ id: string(), attempt: number() })
assert({ id: 'evt_1', attempt: 2, vendor: 'acme' }, Event)

`type()` checks declared fields and allows other keys. Use `object()` when an unknown key should be a validation error.

Separate undefined from null model-null-and-missing

import { nullable, object, optional, string } from 'superstruct'

const Patch = object({
  displayName: optional(string()),
  biography: optional(nullable(string())),
})

`optional()` accepts `undefined`; `nullable()` accepts `null`. Correct inferred optional types require `strictNullChecks`.

Validate an application identifier define-id

import { define, object, string } from 'superstruct'

const OrderId = define('OrderId', value =>
  typeof value === 'string' && /^ord_[a-z0-9]+$/.test(value)
)
const Order = object({ id: OrderId, status: string() })

`define()` reports the supplied name when its predicate returns false. Keep that name stable if clients map error types.

Reject blank text with a message refine-string

import { refine, string } from 'superstruct'

const NonBlank = refine(string(), 'NonBlank', value =>
  value.trim().length > 0 || 'Expected a non-blank string'
)

A refinement can return a message string instead of `false`, which becomes part of the `StructError` failure.

Check arbitrary string keys validate-record

import { assert, integer, record, string } from 'superstruct'

const Counters = record(string(), integer())
assert({ queued: 3, running: 1 }, Counters)

`record()` validates arbitrary key and value pairs. `object()` describes a fixed set of property names.

Accept one of two event shapes accept-union

import { literal, number, object, string, union } from 'superstruct'

const Event = union([
  object({ type: literal('created'), id: string() }),
  object({ type: literal('retried'), attempt: number() }),
])

`union()` tries each struct and reports failure if none match. Distinct literal tags make downstream narrowing clearer.

Derive a TypeScript type infer-type

import { Infer, object, optional, string } from 'superstruct'

const Account = object({ id: string(), nickname: optional(string()) })
type Account = Infer<typeof Account>

`Infer` follows the struct when `strictNullChecks` is enabled. Recursive `lazy()` definitions commonly need an explicit type annotation.

Alternatives

PackageRegistryPick it when
zodnpmUse Zod when ecosystem breadth, frequent releases, and TypeScript-first integrations outweigh package size.
valibotnpmUse Valibot when modular imports and a current tree-shakeable schema API are priorities.
yupnpmUse Yup for form-oriented coercion, conditional fields, and a long-established object schema style.
ajvnpmUse Ajv when JSON Schema is the portable source of truth across services and languages.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.