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.
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
| Install | ✓ · 0.6s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package |
| Browser | 3.5 KB | gzipped (10.5 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 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.
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.
- 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.
- Schemas must become JSON Schema, OpenAPI, or contracts shared with another language. Superstruct schemas are executable JavaScript values.
- You expect built-in email, UUID, URL, and domain rules with published semantics. The project intentionally expects custom types for application formats.
- TypeScript cannot enable `strictNullChecks`. The official type guide requires it for correct optional-property inference.
- Defaults and transformations are expected on every check. `assert` and `is` inspect the input unchanged; use `create`, `mask`, or coercing `validate` deliberately.
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 valueThe 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
| Package | Registry | Pick it when |
|---|---|---|
| zod | npm | Use Zod when ecosystem breadth, frequent releases, and TypeScript-first integrations outweigh package size. |
| valibot | npm | Use Valibot when modular imports and a current tree-shakeable schema API are priorities. |
| yup | npm | Use Yup for form-oriented coercion, conditional fields, and a long-established object schema style. |
| ajv | npm | Use 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.

