superstruct
Superstruct is a runtime data-validation library for JavaScript and TypeScript. You compose small struct objects that describe values, arrays, objects, unions, records, and custom refinements, then assert, test, validate, coerce, or mask unknown input. Successful checks narrow TypeScript types, while failures expose a StructError with the failing value, key, path, branch, refinement, and an iterator over all failures. It works in browsers and Node and has no runtime dependencies.
Superstruct remains a clean, small validator with unusually understandable composition and error inspection. Its inactive release cadence is the reason not to choose it for a new long-lived TypeScript platform unless the API is a distinctly better fit than Zod or Valibot.
Use it if
- You want small composable validators with detailed paths and custom refinements rather than a large batteries-included schema package
- You need both throwing assertions, Boolean type guards, and tuple-style validation from the same schema
- You want to strip unknown object properties with mask or deliberately permit them with type
- You value a dependency-free package with ESM, CommonJS, browser, and TypeScript support
- You want the most active TypeScript schema ecosystem and integrations: Superstruct 2.0.2 last published in July 2024 and the repository's last push was October 2024
- You need JSON Schema generation, OpenAPI tooling, or cross-language schemas; Superstruct definitions are executable JavaScript objects, not a portable schema format
- You expect rich built-in string formats: the core intentionally favors primitive types and asks you to define application-specific email, UUID, and similar checks
- Your TypeScript project cannot enable strictNullChecks; the official type docs warn that optional structs require it for correct inferred types
- You need coercion on every validation call by default: assert and is validate as-is, while defaults and transformations run only through create, mask, or validate with coerce enabled
Setup reality
npm install superstruct adds no runtime dependencies, no peer dependencies, and ships TypeScript declarations plus ESM and CommonJS builds. The current 2.0.2 package declares Node 14 or newer, although browser bundlers can consume its module build. There is no compiler, plugin, provider, or config file. The work is designing schemas and choosing the correct entry operation. object is strict about extra properties and will fail when an API adds an unexpected key; type allows additional properties, while mask returns a copy with unknown keys removed only when used with strict object-style schemas. assert throws and narrows, is returns a Boolean type guard, and validate returns an error/value tuple. Coercions and defaulted values do not run through assert or is. Call create, mask, or validate with { coerce: true } when you actually want transformation. That distinction is an easy production bug because a schema can look correct while a default never applies. StructError.failures() provides every failure, but converting paths and messages into a public API error format is your job. number accepts Infinity, date rejects invalid Date instances, and regexp validates a RegExp object rather than testing a string against it; use refinements such as pattern for content constraints. optional permits undefined, nullable permits null, and they are not interchangeable. TypeScript users should enable strict or at least strictNullChecks. Inference works well for ordinary structs, but recursive lazy structures need an explicit type. The repository has been quiet since 2024, so pin the version, cover schema behavior with tests, and compare the migration cost to Zod or Valibot before making it foundational in a new codebase.
Patterns
Assert an unknown objectassert-object-shape
import { assert, object, array, number, string } from 'superstruct'
const Article = object({
id: number(),
title: string(),
tags: array(string()),
})
assert(payload, Article)
console.log(payload.title)object rejects unknown properties. After assert succeeds, TypeScript narrows payload to the inferred shape.
Check data without throwingtest-with-type-guard
import { is, object, number, string } from 'superstruct'
const User = object({ id: number(), name: string() })
if (is(input, User)) {
console.log(input.name)
}is validates but does not run coercions or apply default values.
Return every validation failurecollect-validation-failures
import { validate } from 'superstruct'
const [error, value] = validate(input, User)
if (error) {
const problems = [...error.failures()].map(({ path, message }) => ({
path: path.join('.'),
message,
}))
return { ok: false, problems }
}
return { ok: true, value }The tuple contains an error or a typed value; do not read value on the error branch.
Create a value with defaultsapply-default-values
import { create, defaulted, number, object, string } from 'superstruct'
const Page = object({
query: string(),
limit: defaulted(number(), 20),
})
const page = create({ query: 'printers' }, Page)Defaults run through create. Calling assert or is on the same input will not fill limit.
Mask keys outside a strict schemastrip-unknown-properties
import { mask, object, number, string } from 'superstruct'
const PublicUser = object({ id: number(), name: string() })
const clean = mask({ id: 1, name: 'Ada', admin: true }, PublicUser)
// { id: 1, name: 'Ada' }mask with type retains extra properties; use object when stripping is intended.
Distinguish missing and null fieldsallow-optional-nullable
import { nullable, object, optional, string } from 'superstruct'
const ProfilePatch = object({
displayName: optional(string()),
biography: optional(nullable(string())),
})optional accepts undefined and nullable accepts null. Enable strictNullChecks for correct TypeScript inference.
Define an application-specific stringdefine-custom-type
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 returns a validation failure when the predicate is false; choose a stable name for useful error messages.
Add a readable refinementrefine-string-value
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 instead of only false, which improves StructError output.
Validate a string-keyed recordvalidate-record-values
import { integer, record, string } from 'superstruct'
const Counters = record(string(), integer())
assert({ queued: 3, running: 1 }, Counters)record validates arbitrary keys and values; object is for a fixed set of named properties.
Infer a TypeScript type from a structinfer-typescript-type
import { Infer, object, optional, string } from 'superstruct'
const Account = object({
id: string(),
nickname: optional(string()),
})
type Account = Infer<typeof Account>Recursive lazy structs cannot always be inferred and need an explicit TypeScript type.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| zod | npm | You want the largest TypeScript-first validation ecosystem, frequent maintenance, and broad framework support |
| valibot | npm | You want a modular tree-shakeable schema library with a current TypeScript-focused API |
| yup | npm | You need familiar object and form validation with extensive coercion and conditional schema behavior |
| ajv | npm | Your source of truth is JSON Schema and schemas must work across languages and tooling |