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

@liveschema/core

v1.2.4

Published

Build a typed, branching schema from any Standard Schema validators. Inferred values are a discriminated union over reachable branches; at runtime, get the ordered list of currently-reachable fields. Works for conditional forms (single-page or multi-step)

Readme

@liveschema/core

A typed schema for conditional forms — declare which fields appear when, in one place, and get back:

  • The live list of fields to render given the current values.
  • A per-branch-narrowed value type (a discriminated union — fields gated by an equality .when() are required in their branch).
  • A Standard Schema validator for form libraries and backend endpoints.

Bring your own form library (React Hook Form, TanStack Form, vee-validate, plain state) and renderer. liveschema owns the branching logic; the UI stays yours.

Using React or Vue? Reach for @liveschema/react or @liveschema/vue instead — they wrap this package in a useLiveSchema() hook/composable that re-derives the reachable-field set as form state changes.

Motivation

Conditional forms scatter business logic. The rules for which fields apply when get duplicated across JSX/template v-if gates, the validation schema, and any backend checks — three places that quietly drift apart. liveschema centralizes that logic in one typed schema: the fields you render, the inferred value type, and the validator all derive from it. Change a branch once, and everything follows.

That's the gap left by today's tooling:

  • Survey libraries (SurveyJS, JSON-Schema-based) — branching is built in, but they own the renderer and return untyped output.
  • Form libraries (React Hook Form, TanStack Form, vee-validate, …) — headless and typed, but the branching logic ends up scattered across JSX/template gates, and TypeScript can't narrow required fields per branch.

liveschema centralizes business logic into single place and allows typed end-to-end type safety.

Install

pnpm add @liveschema/core
# plus whichever Standard-Schema-compliant validator you prefer:
pnpm add zod        # or: valibot, arktype, effect, ...

@liveschema/core has no runtime dependency on any specific validation library. Bring your own.

Quick start

import { z } from 'zod'
import { defineSchema, reachableFields, type InferSchema } from '@liveschema/core'

const schema = defineSchema()
  .field('name', z.string().min(1))
  .field('animal', z.enum(['dog', 'cat']))
  .when({ animal: 'dog' }, (b) => b.field('dogSize', z.enum(['small', 'large'])))
  .when({ animal: 'cat' }, (b) => b.field('indoor', z.boolean()))

type Values = InferSchema<typeof schema>

// Given current values, get the live ordered list of reachable fields:
const fields = reachableFields(schema, { name: 'Ada', animal: 'dog' })
// [
//   { key: 'name',    schema: <standard schema>, value: 'Ada' },
//   { key: 'animal',  schema: <standard schema>, value: 'dog' },
//   { key: 'dogSize', schema: <standard schema>, value: undefined },
// ]

Compared to a Zod discriminated union

You can model the same branching as a hand-written z.discriminatedUnion. But every variant has to restate the shared fields, and the duplication compounds with each shared field and each branch:

// Plain Zod — `name` (and any other shared field) repeated in every variant
const schema = z.discriminatedUnion('animal', [
  z.object({
    name: z.string().min(1),
    animal: z.literal('dog'),
    dogSize: z.enum(['small', 'large']),
  }),
  z.object({ name: z.string().min(1), animal: z.literal('cat'), indoor: z.boolean() }),
])
// liveschema — shared fields declared once, branches added with `.when()`
const schema = defineSchema()
  .field('name', z.string().min(1))
  .field('animal', z.enum(['dog', 'cat']))
  .when({ animal: 'dog' }, (b) => b.field('dogSize', z.enum(['small', 'large'])))
  .when({ animal: 'cat' }, (b) => b.field('indoor', z.boolean()))

InferSchema<typeof schema> is the identical discriminated union (name in both branches, dogSize only on 'dog', indoor only on 'cat'). The difference is everything around the type:

  • No repetition — shared fields are declared once, not restated per variant.
  • Branch only where it matters — write a .when() only for the values that add fields. Drop .when({ animal: 'cat' }, …) entirely and 'cat' is still valid as the base record; a discriminatedUnion rejects any discriminant value you don't spell out as a full variant.
  • Any condition, not just literals — branches can be predicates or cross-field checks (.when((v) => …)), which a literal-discriminant union can't express at all.

API

| Method | Fires at runtime when | Effect on the inferred type | | ---------------------------------- | -------------------------------- | ---------------------------------------------------------------- | | .field(key, schema) | always | adds the field | | .when({k: literal, ...}, b => …) | every listed key matches literal | new fields required in matching variants (narrows the union) | | .whenAny([p1, p2, …], b => …) | any pattern matches | required in statically-matching variants, optional in others | | .when((v) => boolean, b => …) | predicate is truthy | optional (TS can't introspect predicates) |

Wrap multiple subfields in an object schema to group them — the value nests:

.field('owner', z.object({ name: z.string().min(1), email: z.email() }))
// → values.owner.name

Inside a narrowed branch, fields that were optional in the union become required:

function handle(v: InferSchema<typeof schema>) {
  if (v.animal === 'dog') {
    const size: 'small' | 'large' = v.dogSize // required, not optional
  }
}

Use Partial<Values> to model in-progress (partially-filled) state.

Backend validation

toStandardSchema(schema) returns a Standard Schema validator that covers the currently-reachable fields. Plug it into any framework that accepts Standard Schema:

import { toStandardSchema } from '@liveschema/core'

const standard = toStandardSchema(schema)
let result = standard['~standard'].validate(await req.json())
if (result instanceof Promise) result = await result
if (result.issues) return new Response(JSON.stringify(result.issues), { status: 422 })
// result.value is fully typed and only contains reachable fields

The validator rebuilds result.value from scratch each call, so abandoned-branch values (e.g. dogSize after animal switched from 'dog' to 'cat') are pruned for free — both on the backend and inside form libraries that submit through it. If you're managing raw state yourself, filter against reachableFields:

const keep = new Set(reachableFields(schema, values).map((f) => f.key))
const cleaned = Object.fromEntries(Object.entries(values).filter(([k]) => keep.has(k)))

Rendering the reachable fields

Two rendering shapes work well; both are driven from the same schema.

1. Static layout, gate each field by reachability (default)

Most forms have a small, known set of fields with distinct markup per field. Lay out every field statically and gate visibility per-field on whether it's currently reachable. With raw core, build a keyed lookup from reachableFields(...) — a field is reachable exactly when it's present in the result:

import { reachableFields, enumOptions, type SchemaField } from '@liveschema/core'

const reachable = Object.fromEntries(
  reachableFields(schema, values).map((f) => [f.key, f]),
) as Partial<Record<FieldKey, SchemaField<FieldKey>>>

// pseudo-JSX — one block per field, rendered only when reachable:
return (
  <>
    {reachable.email && <TextInput name="email" value={values.email} />}
    {reachable.orderType && (
      <RadioGroup name="orderType" options={enumOptions(reachable.orderType.schema) ?? []} />
    )}
    {reachable.leaveAtDoor && <Checkbox name="leaveAtDoor" />}
    {/* ... */}
  </>
)

End-to-end with raw core: examples/vanilla-example/src/main.ts.

If you're using @liveschema/react / @liveschema/vue, the hook hands you this directly — a keyed reachableFields record plus an isReachableField(key) predicate to gate with (and a fields record carrying isReachable for every declared field, if you'd rather render unreachable fields as disabled than unmount them). End-to-end: examples/tanstack-form-example/src/FormPage.tsx.

2. Dynamic for-loop with a renderer map

When you have many similar fields (one-field-per-screen wizards, dense surveys) and don't need to customize each field, you can iterate reachableFields(...) and dispatch each field to a small renderer keyed by field name:

import { Fragment } from 'react'
import type { SchemaField } from '@liveschema/core'

type Renderer = (field: SchemaField<FieldKey>) => ReactNode

const renderers: Record<FieldKey, Renderer> = {
  email: (f) => <TextStep path={f.key} type="email" />,
  fullName: (f) => <TextStep path={f.key} />,
  orderType: (f) => <RadioStep path={f.key} options={enumOptions(f.schema) ?? []} />,
  leaveAtDoor: (f) => <CheckboxStep path={f.key} />,
  pizzaCount: (f) => <NumberStep path={f.key} min={1} max={20} />,
  // ...
}

return (
  <>
    {reachableFields(schema, values).map((field) => (
      <Fragment key={field.key}>{renderers[field.key](field)}</Fragment>
    ))}
  </>
)

End-to-end examples:

API

| Export | Purpose | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | defineSchema() | Start a schema builder | | .field(key, schema) | Declare a field (schema is any Standard Schema validator) | | .when(pattern, branch) | Equality-gated sub-branch | | .when(predicate, branch) | Predicate-gated sub-branch | | .whenAny(patterns, branch) | OR-gated sub-branch | | reachableFields(schema, values) | Ordered list of currently-reachable fields | | declaredFields(schema, values) | Every declared field in source order, each tagged with isReachable — useful when you want to render gated fields as disabled rather than unmount them | | validateSchema(schema, values) | { key: firstMessage } errors for reachable fields — plug straight into Formik/vee-validate/etc. validate | | toStandardSchema(schema) | One Standard Schema validating the currently-reachable fields — for TanStack Form's onDynamic, react-hook-form's standard-schema resolver, backend request validation, etc. | | enumOptions(schema) | Best-effort enum option list (undefined for non-enum schemas) — handy for rendering radios/selects | | InferSchema<F> | Discriminated-union value type | | InferField<F, K> | Type of a single field across variants | | FlatInferSchema<F> | Flat, fully-optional view of the value type — single record indexable by any field key (for in-progress form state with libraries that want one shape, e.g. RHF) | | SchemaKeys<F> | Union of every field key across every variant | | SchemaField<K> | { key, schema, value } returned by reachableFields | | DeclaredField<K> | { key, schema, isReachable, value } returned by declaredFields | | SchemaErrors<F> | Return shape of validateSchemaPartial<Record<SchemaKeys<F>, string>> |

What this package is not

  • Not a form library — bring your own (TanStack Form, vee-validate, React Hook Form, plain state). The package gives you the reachable-field list; you keep the values.
  • Not a UI library — render however you want, keyed by field.key.
  • Doesn't track navigation or phase (e.g. "fill" vs "review", current-step index) — that's all in the consumer.
  • Doesn't route validation errors into form-library field state — the consumer does that with the issues from field.schema['~standard'].validate(values[field.key]).