Skip to content

feat(validation): type inference for validators and schema builders - #17

Draft
halvardssm wants to merge 6 commits into
mainfrom
vibe/validator-type-inference
Draft

feat(validation): type inference for validators and schema builders#17
halvardssm wants to merge 6 commits into
mainfrom
vibe/validator-type-inference

Conversation

@halvardssm

Copy link
Copy Markdown
Owner

Summary

  • Add Zod-inspired type inference for the validation namespace.
  • New validation/infer.ts with InferInput/InferOutput plus composition helpers
    (InferMemberOutput, InferObjectOutput, InferCombinationOutput) that read
    the schema's ~standard.types field.
  • The schema() builder now sets ~standard.types, so the inferred input/output
    types resolve to the schema's Input/Output (the generic params alone cannot
    infer Input because it is structurally absent from StandardSchemaV1).
  • array, object, and combination builders are now generic over their
    options, so composed types are inferred:
    • array({ items: string() }) -> string[]
    • object({ properties }) -> object shape (all properties optional, since
      JSON Schema's required is string[] and widens array literals)
    • combination({ anyOf }) -> union of member output types
  • validate/validateAsync/parse/parseAsync accept unknown input
    (matching Zod's parse(data: unknown): Output) and return
    Result<InferOutput<S>> / InferOutput<S> instead of unknown.
  • Re-export InferInput/InferOutput (and helpers) from the package entrypoint.
  • Add validation/infer.test.ts with compile-time IsExact assertions for
    scalars, array, object, combination, and the parse/validate signatures.
  • Document type inference in validation/README.md.

Verification

  • Type-checked all validation/ source + test files with tsc against the real
    @standard-schema/spec type definitions (0 errors).
  • Ran the existing runtime behavior through Node (--experimental-strip-types):
    validator.test.ts (12/12), utils.test.ts (14/14), and a broad
    json_schema.test.ts subset covering scalars, formats, arrays (prefixItems,
    contains, uniqueItems), nested objects (additionalProperties, patternProperties,
    minProperties), and combinations (allOf/anyOf/oneOf/not) — all pass with
    behavior unchanged. New infer.test.ts runtime sanity passes and its
    compile-time assertions hold.

Note: deno task check / deno task test were not run in this sandbox
(Deno is not installed here); CI will run them. The added ~standard.types
field uses undefined runtime values as a type-level carrier (per the
Standard Schema spec), and existing assertObjectMatch schema-shape checks
use partial matching, so they remain green.

mistral-vibe and others added 6 commits August 14, 2026 14:40
Implement Zod-inspired type inference for the validation namespace:

- Add `validation/infer.ts` with `InferInput`/`InferOutput` and composition
  helpers (`InferMemberOutput`, `InferObjectOutput`,
  `InferCombinationOutput`) that read `~standard.types`.
- Set `~standard.types` in the `schema()` builder so the inferred input/output
  types resolve to the schema's `Input`/`Output` (the generic params alone cannot
  infer `Input` since it is structurally absent from `StandardSchemaV1`).
- Make `array`, `object` and `combination` builders generic over their options
  so composed types are inferred: `array({ items: string() })` -> `string[]`,
  `object({ properties })` -> shape (all properties optional, since JSON
  Schema's `required` is `string[]` and widens), `combination({ anyOf })` ->
  union of member outputs.
- `validate`/`parse` accept `unknown` input (matching Zod's `parse(data:
  unknown): Output`) and return `Result<InferOutput<S>>` / `InferOutput<S>`.
- Re-export `InferInput`/`InferOutput` (and helpers) from the package entrypoint.
- Add `validation/infer.test.ts` with compile-time `IsExact` assertions.
- Document type inference in `validation/README.md`.

Verified with `tsc` against the real `@standard-schema/spec` types and runtime
tests; existing behavior is preserved.

Co-authored-by: halvardssm <halvardssm@users.noreply.github.com>
Co-authored-by: halvardssm <halvardssm@users.noreply.github.com>
JSON Schema objects may carry arbitrary keys (constrained at runtime by
additionalProperties/unevaluatedProperties/patternProperties), so the inferred
object output now includes a [key: string]: unknown index signature. This keeps
declared property typing while preventing excess-property type errors in callers
and tests that pass objects with additional keys. Add a compile-time test for it.

Co-authored-by: halvardssm <halvardssm@users.noreply.github.com>
Extend array type inference to cover prefixItems, unevaluatedItems, and
contains, not just items:

- array({ items: string() }) -> string[]
- array({ prefixItems: [string(), number()] }) -> [string, number] (exact tuple)
- array({ prefixItems: [string()], items: number() }) -> [string, ...number[]]
- array({ prefixItems: [string()], unevaluatedItems: number() }) -> [string, ...number[]]
- array({ prefixItems: [string(), number()], contains: boolean() }) ->
  [string, number, ...boolean[]]
- array({ contains: number() }) / array({ unevaluatedItems: number() }) -> T[]
- array() -> unknown[]

When prefixItems is present, the leading elements form a fixed tuple; when a
variadic rest source (items/unevaluatedItems/contains) is also present it is
appended as a variadic tail, otherwise the tuple is exact. Tuple preservation
relies on a `const Prefix` type parameter (TS 5.0+).

Add InferArrayTuple/InferArrayRest/InferArrayHasRest/InferArrayOutput helpers in
validation/infer.ts (re-exported from the package entrypoint), update the
array() builder to a two-generic signature, and add compile-time tests in
validation/infer.test.ts.

Verified locally with tsc (against the real @standard-schema/spec types), tsx,
deno fmt --check, and deno lint; existing runtime behavior is preserved.

Co-authored-by: halvardssm <halvardssm@users.noreply.github.com>
…dditionalProperties

Extend object type inference to use `properties`, `required`, and
`additionalProperties`:

- Keys listed in `required` are required; the remaining declared keys are
  optional.
- `additionalProperties: false` disallows extra (undeclared) keys (a `never`
  index signature forbids them); a schema or `true`/absent allows extras,
  typed `unknown` to avoid unsound conflicts with declared properties of a
  different type (the `additionalProperties` schema still validates extras at
  runtime).

`required` is captured as a `const` tuple on the builder (TS 5.0+) so the
literal keys are preserved rather than widened to `string[]`.

Add InferObjectAdditionalIndex/InferObjectRequiredKeys helpers and a
three-parameter InferObjectOutput in validation/infer.ts (re-exported from the
package entrypoint), update the object() builder to a two-generic signature,
and update compile-time tests in validation/infer.test.ts.

Verified locally with tsc (against the real @standard-schema/spec types), tsx,
deno fmt --check, and deno lint; existing runtime behavior is preserved.

Co-authored-by: halvardssm <halvardssm@users.noreply.github.com>
- Replace runtime-dereferencing assertions in infer.test.ts (which crashed on
  null) with IsSubtype type-level checks that have no runtime side effects.
- additionalProperties: false now contributes no index signature ({} ) rather
  than a [key: string]: never index, which was unsound (it forbade declared
  properties too). With no index signature, declared keys form a strict shape
  and excess-property checks forbid undeclared keys on object literals.

Co-authored-by: halvardssm <halvardssm@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants