diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..ea7dc64
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,64 @@
+# Plan: Type Inference for Validator Functions
+
+## Goal
+
+Implement type inference for the validator functions (`validate`,
+`validateAsync`, `parse`, `parseAsync`) and schema builders in the `validation`
+namespace, drawing inspiration from Zod. The input type of
+`validate(schema, input)` and the return type of `parse(schema, input)` should
+be inferred from the schema, including for composed schemas (`array`, `object`,
+`combination`, `nullable`).
+
+## Background
+
+- Schemas implement `StandardSchemaV1` (a generic interface) but
+ the builders in `json_schema.ts` did NOT set the `~standard.types` field. As a
+ result `StandardSchemaV1.InferInput`/`InferOutput` (which read
+ `~standard.types`) resolved to `unknown` for these schemas.
+- `validator.ts` used `StandardSchemaV1.InferInput` / `InferOutput` plus a
+ union with `unknown`, so callers never got useful input typing and `parse`
+ returned `unknown`.
+- Zod exposes inference via typed schemas + `z.infer`. The Standard Schema
+ equivalent is the `~standard.types` field plus the generic
+ `StandardSchemaV1` parameters.
+
+## Tasks
+
+-
+ 1. [x] Add inference helpers in `validation/infer.ts` exposing `InferInput`
+ and `InferOutput` that read `~standard.types` (set by the schema
+ builder) and fall back to `unknown`, plus composition helpers
+ (`InferMemberOutput`, `InferObjectOutput`, `InferCombinationOutput`).
+-
+ 2. [x] Update `validator.ts` (`validate`, `validateAsync`, `parse`,
+ `parseAsync`) to use the new `InferInput`/`InferOutput` helpers so
+ input is typed and parsed output carries the schema's output type. Kept
+ the existing `boolean` schema shortcut and async support behavior.
+-
+ 3. [x] Set `~standard.types` in the `schema()` builder so inference resolves
+ to the schema's `Input`/`Output` (the generic params alone cannot infer
+ `Input` because it is structurally absent from `StandardSchemaV1`).
+-
+ 4. [x] Make schema builders in `json_schema.ts` carry proper composed types:
+ - [x] 4a. `array({ items })` -> `InferOutput[]` (input/output).
+ - [x] 4b. `object({ properties })` -> object shape with all properties
+ optional (JSON Schema's `required` is `string[]` and widens, so it
+ cannot mark keys required at the type level).
+ - [x] 4c. `combination({ allOf/anyOf/oneOf })` -> union of member output
+ types.
+ - [x] 4d. `nullable()` already typed as `null`; left as is.
+-
+ 5. [x] Add type-level tests (`validation/infer.test.ts`) using compile-time
+ `IsExact`/`IsSubtype` assertions for scalars, `array`, `object`,
+ `combination`, and `parse`/`validate` signatures.
+-
+ 6. [x] Verified with `tsc` (against the real `@standard-schema/spec` types)
+ and runtime tests via Node (`--experimental-strip-types`): all type
+ checks pass and all existing runtime behavior is preserved.
+
+## Non-goals
+
+- No new runtime behavior changes beyond setting the `~standard.types` carrier
+ field (whose runtime values are `undefined`; it is a type-level carrier).
+- No changes to JSON Schema output/input converters.
+- No changes to other packages.
diff --git a/validation/README.md b/validation/README.md
index 388871c..17e56be 100644
--- a/validation/README.md
+++ b/validation/README.md
@@ -150,3 +150,65 @@ const outputSchema = getStandardJSONSchemaV1Output(mySchema, {
target: "draft-2020-12",
});
```
+
+### Type Inference
+
+The schema builders and validator functions are fully typed. The input type of
+`validate`/`parse` and the return type of `parse` are inferred from the schema,
+including for composed schemas (`array`, `object`, `combination`).
+
+```ts
+import {
+ array,
+ combination,
+ InferInput,
+ InferOutput,
+ number,
+ object,
+ parse,
+ string,
+} from "@stdext/validation";
+
+// Scalars infer their own type
+const str = string();
+type T = InferOutput; // string
+const parsed: string = parse(str, "hello");
+
+// Arrays infer the element type
+const tags = array({ items: string() });
+type Tags = InferOutput; // string[]
+const arr: string[] = parse(tags, ["a", "b"]);
+
+// prefixItems infers a fixed tuple, and items/unevaluatedItems/contains append
+// a variadic tail
+const tuple = array({ prefixItems: [string(), number()] });
+type Tuple = InferOutput; // [string, number]
+const t: [string, number] = parse(tuple, ["a", 1]);
+
+const tupleRest = array({ prefixItems: [string()], items: number() });
+type TupleRest = InferOutput; // [string, ...number[]]
+const tr: [string, ...number[]] = parse(tupleRest, ["a", 1, 2, 3]);
+
+// Objects infer their shape from `properties`, `required`, and
+// `additionalProperties`. `required` keys become required; the rest are
+// optional. `additionalProperties: false` disallows extra keys, a schema/`true`
+// allows them (typed `unknown`).
+const person = object({
+ properties: { name: string(), age: number() },
+ required: ["name"],
+});
+type Person = InferOutput; // { name: string; age?: number } & { [key: string]: unknown }
+
+const strict = object({
+ properties: { name: string() },
+ required: ["name"],
+ additionalProperties: false,
+});
+type Strict = InferOutput; // { name: string }
+
+// Combinations infer a union of their members
+const id = combination({ anyOf: [string(), number()] });
+type Id = InferOutput; // string | number
+```
+
+`InferInput` works the same way to extract a schema's expected input type.
diff --git a/validation/infer.test.ts b/validation/infer.test.ts
new file mode 100644
index 0000000..203f593
--- /dev/null
+++ b/validation/infer.test.ts
@@ -0,0 +1,188 @@
+import {
+ array,
+ boolean,
+ combination,
+ integer,
+ nullable,
+ number,
+ object,
+ string,
+} from "./json_schema.ts";
+import { type InferInput, type InferOutput, parse, validate } from "./mod.ts";
+import { assert } from "@std/assert";
+
+/**
+ * Compile-time type assertion helper.
+ *
+ * Asserts that the type `Actual` is assignable to `Expected` (i.e. `Expected`
+ * is a supertype of `Actual`). If `Actual` is not assignable to `Expected`,
+ * `deno check` fails with an error.
+ */
+type IsSubtype = Actual extends Expected ? true : never;
+
+/**
+ * Compile-time type assertion helper.
+ *
+ * Asserts that `Actual` and `Expected` are exactly the same type by requiring
+ * mutual assignability. Use `IsExact` when the types must match precisely.
+ */
+type IsExact = IsSubtype extends true
+ ? IsSubtype extends true ? true : never
+ : never;
+
+/** Marker const used to force evaluation of a type-level assertion. */
+const ok: true = true;
+
+Deno.test("type inference: scalar schemas", () => {
+ const s = string();
+ const _a: IsExact, string> = ok;
+ const _b: IsExact, string> = ok;
+
+ const n = number();
+ const _c: IsExact, number> = ok;
+
+ const i = integer();
+ const _d: IsExact, number> = ok;
+
+ const b = boolean();
+ const _e: IsExact, boolean> = ok;
+
+ const nu = nullable();
+ const _f: IsExact, null> = ok;
+});
+
+Deno.test("type inference: array schema", () => {
+ const s = array({ items: string() });
+ const _a: IsExact, string[]> = ok;
+ const _b: IsExact, string[]> = ok;
+
+ const n = array({ items: number() });
+ const _c: IsExact, number[]> = ok;
+
+ // Array without items falls back to unknown[]
+ const u = array();
+ const _d: IsExact, unknown[]> = ok;
+
+ // prefixItems infers a fixed tuple
+ const tuple = array({ prefixItems: [string(), number()] });
+ const _e: IsExact, [string, number]> = ok;
+
+ // prefixItems + items appends a variadic tail to the tuple
+ const tupleRest = array({ prefixItems: [string()], items: number() });
+ const _f: IsExact, [string, ...number[]]> = ok;
+
+ // prefixItems + unevaluatedItems appends a variadic tail to the tuple
+ const tupleUnevaluated = array({
+ prefixItems: [string()],
+ unevaluatedItems: number(),
+ });
+ const _g: IsExact<
+ InferOutput,
+ [string, ...number[]]
+ > = ok;
+
+ // prefixItems + contains appends a variadic tail to the tuple
+ const tupleContains = array({
+ prefixItems: [string(), number()],
+ contains: boolean(),
+ });
+ const _h: IsExact<
+ InferOutput,
+ [string, number, ...boolean[]]
+ > = ok;
+
+ // contains (without prefixItems) infers a uniform array
+ const contains = array({ contains: number() });
+ const _i: IsExact, number[]> = ok;
+
+ // unevaluatedItems (without prefixItems) infers a uniform array
+ const unevaluated = array({ unevaluatedItems: number() });
+ const _j: IsExact, number[]> = ok;
+});
+
+Deno.test("type inference: object schema", () => {
+ // required drives required vs optional keys
+ const s = object({
+ properties: {
+ name: string(),
+ age: number(),
+ },
+ required: ["name"],
+ });
+ // name is required, age is optional; extras allowed as unknown
+ const _a: IsSubtype<{ name: string; age?: number }, InferOutput> =
+ ok;
+ const _b: IsSubtype<
+ InferOutput,
+ { name: string; age?: number; [k: string]: unknown }
+ > = ok;
+
+ // all required + additionalProperties: false
+ const all = object({
+ properties: { a: string(), b: boolean() },
+ required: ["a", "b"],
+ additionalProperties: false,
+ });
+ const _c: IsSubtype<{ a: string; b: boolean }, InferOutput> = ok;
+
+ // no required -> all optional
+ const opt = object({ properties: { x: string(), y: number() } });
+ const _d: IsSubtype<{ x?: string; y?: number }, InferOutput> = ok;
+
+ // additionalProperties: false disallows extras (strict shape)
+ const strict = object({
+ properties: { name: string() },
+ required: ["name"],
+ additionalProperties: false,
+ });
+ const _e: IsSubtype<{ name: string }, InferOutput> = ok;
+
+ // additionalProperties: allows extras (typed unknown to avoid
+ // conflicts with declared properties of a different type)
+ const extras = object({
+ properties: { name: string() },
+ required: ["name"],
+ additionalProperties: number(),
+ });
+ const _f: IsSubtype<
+ { name: string; extra: number },
+ InferOutput
+ > = ok;
+});
+
+Deno.test("type inference: combination schema", () => {
+ const s = combination({ anyOf: [string(), number()] });
+ const _a: IsExact, string | number> = ok;
+
+ const one = combination({ oneOf: [string(), boolean()] });
+ const _b: IsExact, string | boolean> = ok;
+});
+
+Deno.test("type inference: parse and validate signatures", () => {
+ const s = string();
+ const parsed = parse(s, "hello");
+ const _a: IsExact = ok;
+
+ // Array parse infers element type
+ const arr = array({ items: string() });
+ const arrParsed = parse(arr, ["a", "b"]);
+ const _b: IsExact = ok;
+
+ // Object parse infers shape (required keys are required)
+ const obj = object({
+ properties: { id: number(), label: string() },
+ required: ["id", "label"],
+ additionalProperties: false,
+ });
+ const objParsed = parse(obj, { id: 1, label: "x" });
+ const _c: IsExact = ok;
+
+ // validate result carries the output type
+ const result = validate(s, "hello");
+ if (!result.issues) {
+ const _d: IsExact = ok;
+ }
+
+ // Sanity: the assertions above are all compile-time; keep deno test happy.
+ assert(parsed === "hello");
+});
diff --git a/validation/infer.ts b/validation/infer.ts
new file mode 100644
index 0000000..cb6fe35
--- /dev/null
+++ b/validation/infer.ts
@@ -0,0 +1,258 @@
+import type { StandardSchemaV1 } from "@standard-schema/spec";
+
+/**
+ * Extracts the input type of a Standard Schema.
+ *
+ * Reads the schema's `~standard.types` field (set by the schema builders in
+ * `./json_schema.ts`), falling back to the `StandardSchemaV1`
+ * type parameters, and finally to `unknown`. Schemas built by this package
+ * always set `~standard.types`, so this resolves to the correct input type for
+ * them; for foreign schemas that do not set `types` the result is `unknown`
+ * (matching the spec's own `InferInput`).
+ *
+ * @template S - The schema type
+ *
+ * @example
+ * ```typescript
+ * const s = string();
+ * type In = InferInput; // string
+ * ```
+ */
+export type InferInput =
+ // Prefer the explicitly declared `~standard.types` when available...
+ S extends { "~standard": { types?: { input: infer I } } } ? I
+ // ...otherwise derive from the StandardSchemaV1 parameters.
+ : S extends StandardSchemaV1 ? I
+ : unknown;
+
+/**
+ * Extracts the output type of a Standard Schema.
+ *
+ * Reads the schema's `~standard.types` field (set by the schema builders in
+ * `./json_schema.ts`), falling back to the `StandardSchemaV1`
+ * type parameters, and finally to `unknown`. Schemas built by this package
+ * always set `~standard.types`, so this resolves to the correct output type for
+ * them; for foreign schemas that do not set `types` the result is `unknown`
+ * (matching the spec's own `InferOutput`).
+ *
+ * @template S - The schema type
+ *
+ * @example
+ * ```typescript
+ * const s = string();
+ * type Out = InferOutput; // string
+ * ```
+ */
+export type InferOutput = S extends
+ { "~standard": { types?: { output: infer O } } } ? O
+ : S extends StandardSchemaV1 ? O
+ : unknown;
+
+/**
+ * Resolves the output type of a {@link SchemaObject} member of an array or
+ * object schema, where the member may be a schema or a literal `boolean`
+ * (JSON Schema's `true`/`false` shorthand).
+ *
+ * - A `false` member always fails, contributing `never`.
+ * - A `true` member accepts anything, contributing `unknown`.
+ * - A schema member contributes its inferred output type.
+ *
+ * @template S - The member schema or boolean
+ *
+ * @example
+ * ```typescript
+ * type A = InferMemberOutput;
+ * ```
+ */
+export type InferMemberOutput = S extends false ? never
+ : S extends true ? unknown
+ : InferOutput;
+
+/**
+ * Maps a record of member schemas (e.g. an object's `properties`) to a record
+ * of their inferred output types.
+ *
+ * @template T - The record of member schemas
+ *
+ * @example
+ * ```typescript
+ * const props = { name: string(), age: number() };
+ * type Out = InferMemberOutputRecord;
+ * // { name: string, age: number }
+ * ```
+ */
+export type InferMemberOutputRecord = {
+ [K in keyof T]: InferMemberOutput;
+};
+
+/**
+ * Resolves the index-signature contribution of an object schema's
+ * `additionalProperties` option, mirroring JSON Schema 2020-12 semantics:
+ *
+ * - `false` disallows extra properties (no index signature).
+ * - `true` or absent allows any extra property (`unknown`).
+ * - a schema allows extra properties validated against it (its output type).
+ *
+ * @template AdditionalProperties - The `additionalProperties` option value
+ */
+export type InferObjectAdditionalIndex =
+ // `false` disallows extra properties by contributing no index signature, so
+ // the object type consists only of its declared (and required) keys and
+ // excess-property checks forbid undeclared keys on object literals. Any other
+ // value (`true`, a schema, or absent) allows extra properties; the index is
+ // typed `unknown` to avoid unsound conflicts with declared properties of a
+ // different type (the `additionalProperties` schema still validates extras at
+ // runtime).
+ // deno-lint-ignore ban-types
+ AdditionalProperties extends false ? {}
+ : { [key: string]: unknown };
+
+/**
+ * Extracts the union of required property keys from a `required` tuple.
+ *
+ * @template Required - The readonly `required` string tuple
+ */
+export type InferObjectRequiredKeys<
+ Required extends ReadonlyArray,
+> = Required[number];
+
+/**
+ * Builds the output type of an `object` schema from its `properties`,
+ * `required`, and `additionalProperties`.
+ *
+ * - Keys listed in `required` are required; the remaining declared keys are
+ * optional.
+ * - `additionalProperties` controls extra (undeclared) keys: `false` removes
+ * the index signature, a schema types the extra values, and `true`/absent
+ * allows `unknown` extra values.
+ *
+ * `required` must be captured as a `const` tuple on the builder (see
+ * `object()`) so the literal keys are preserved rather than widened to
+ * `string[]`.
+ *
+ * @template Properties - The `properties` record
+ * @template Required - The readonly `required` string tuple
+ * @template AdditionalProperties - The `additionalProperties` option value
+ *
+ * @example
+ * ```typescript
+ * type Out = InferObjectOutput<
+ * { name: StringSchema; age: NumberSchema },
+ * ["name"],
+ * false
+ * >;
+ * // { name: string; age?: number }
+ * ```
+ */
+export type InferObjectOutput<
+ Properties,
+ Required extends ReadonlyArray = [],
+ AdditionalProperties = undefined,
+> =
+ & {
+ [
+ K in keyof Properties as K extends InferObjectRequiredKeys ? K
+ : never
+ ]: InferMemberOutput;
+ }
+ & {
+ [
+ K in keyof Properties as K extends InferObjectRequiredKeys
+ ? never
+ : K
+ ]?: InferMemberOutput;
+ }
+ & InferObjectAdditionalIndex;
+
+/**
+ * Maps a readonly tuple of schemas (e.g. an array's `prefixItems`) to a tuple
+ * of their inferred output types, preserving element order and count.
+ *
+ * @template T - The readonly tuple of member schemas
+ *
+ * @example
+ * ```typescript
+ * type T = InferArrayTuple;
+ * // readonly [string, number]
+ * ```
+ */
+export type InferArrayTuple> = {
+ [K in keyof T]: InferMemberOutput;
+};
+
+/**
+ * Resolves the variadic "rest" element type of an array schema from its
+ * `items`, `unevaluatedItems`, or `contains` option (in that order of
+ * precedence), mirroring JSON Schema 2020-12 evaluation. Returns `never` when
+ * none of these are present.
+ *
+ * @template Options - The array options
+ */
+export type InferArrayRest = Options extends { items: infer Items }
+ ? InferMemberOutput
+ : Options extends { unevaluatedItems: infer Unevaluated } ? InferMemberOutput<
+ Unevaluated
+ >
+ : Options extends { contains: infer Contains } ? InferMemberOutput
+ : never;
+
+/**
+ * Whether an array schema declares any variadic rest element source
+ * (`items`, `unevaluatedItems`, or `contains`).
+ *
+ * @template Options - The array options
+ */
+export type InferArrayHasRest = Options extends { items: infer _Items }
+ ? true
+ : Options extends { unevaluatedItems: infer _Unevaluated } ? true
+ : Options extends { contains: infer _Contains } ? true
+ : false;
+
+/**
+ * Builds the output type of an `array` schema.
+ *
+ * - When `prefixItems` is present, the leading elements form a fixed tuple.
+ * If a variadic rest source (`items`/`unevaluatedItems`/`contains`) is also
+ * present, it is appended as a variadic tail; otherwise the tuple is exact.
+ * - When only a rest source is present, the result is `Rest[]`.
+ * - Otherwise the result is `unknown[]`.
+ *
+ * @template Prefix - The readonly `prefixItems` tuple, or `undefined`
+ * @template Options - The array options (carrying the rest element sources)
+ *
+ * @example
+ * ```typescript
+ * type A = InferArrayOutput;
+ * // [string, number]
+ * type B = InferArrayOutput;
+ * // [string, ...number[]]
+ * ```
+ */
+export type InferArrayOutput<
+ Prefix extends ReadonlyArray | undefined,
+ Options,
+> = Prefix extends ReadonlyArray
+ ? InferArrayHasRest extends true
+ ? [...InferArrayTuple, ...InferArrayRest[]]
+ : [...InferArrayTuple]
+ : InferArrayHasRest extends true ? InferArrayRest[]
+ : unknown[];
+
+/**
+ * Infers the output type of a `combination` schema from its `allOf`, `anyOf`
+ * and `oneOf` members. The resulting type is the union of every member output.
+ * When no members are present the result is `unknown`.
+ *
+ * @template AllOf - The readonly array of `allOf` member schemas
+ * @template AnyOf - The readonly array of `anyOf` member schemas
+ * @template OneOf - The readonly array of `oneOf` member schemas
+ */
+export type InferCombinationOutput<
+ AllOf extends ReadonlyArray | undefined,
+ AnyOf extends ReadonlyArray | undefined,
+ OneOf extends ReadonlyArray | undefined,
+> = [
+ | (AllOf extends ReadonlyArray ? InferMemberOutput : never)
+ | (AnyOf extends ReadonlyArray ? InferMemberOutput : never)
+ | (OneOf extends ReadonlyArray ? InferMemberOutput : never),
+][0] extends infer R ? [R] extends [never] ? unknown : R : unknown;
diff --git a/validation/json_schema.ts b/validation/json_schema.ts
index ed350ee..8d6329b 100644
--- a/validation/json_schema.ts
+++ b/validation/json_schema.ts
@@ -29,6 +29,11 @@ import {
RFC6901_RELATIVE_JSON_POINTER,
stringify,
} from "./utils.ts";
+import type {
+ InferArrayOutput,
+ InferCombinationOutput,
+ InferObjectOutput,
+} from "./infer.ts";
import { validate as _validate } from "./validator.ts";
/**
@@ -263,6 +268,13 @@ function schema<
"~standard": {
version: 1,
vendor: "@stdext/validation",
+ // Expose the inferred types so that `InferInput`/`InferOutput` (and the
+ // spec's `StandardSchemaV1.InferInput`/`InferOutput`) resolve to the
+ // schema's `Input`/`Output` rather than `unknown`.
+ types: {
+ input: undefined as unknown as Input,
+ output: undefined as unknown as Output,
+ },
validate: options.validate,
jsonSchema: {
input: options.input,
@@ -825,10 +837,43 @@ export interface ArrayOptions extends
> {
}
+/**
+ * The inferred output type for an array schema.
+ *
+ * - When `prefixItems` is present, the leading elements form a fixed tuple.
+ * If a variadic rest source (`items`/`unevaluatedItems`/`contains`) is also
+ * present, it is appended as a variadic tail; otherwise the tuple is exact.
+ * - When only a rest source (`items`/`unevaluatedItems`/`contains`) is present,
+ * the result is `Rest[]`.
+ * - Otherwise the result is `unknown[]`.
+ *
+ * @template Prefix - The readonly `prefixItems` tuple, or `undefined`
+ * @template Options - The {@link ArrayOptions} carrying the rest element
+ * sources
+ */
+export type ArrayElementOutput<
+ Prefix extends ReadonlyArray | undefined,
+ Options extends ArrayOptions | undefined,
+> = InferArrayOutput;
+
/**
* Creates an array schema that validates array values.
- * Supports constraints for items, length, and uniqueness.
+ * Supports constraints for items, prefix items, contains, length, and
+ * uniqueness.
*
+ * Type inference:
+ * - `items` infers a uniform array type (e.g. `array({ items: string() })` ->
+ * `string[]`).
+ * - `prefixItems` infers a fixed tuple (e.g. `array({ prefixItems: [string(),
+ * number()] })` -> `[string, number]`).
+ * - `prefixItems` combined with `items`, `unevaluatedItems`, or `contains`
+ * appends a variadic tail to the tuple (e.g. `array({ prefixItems:
+ * [string()], items: number() })` -> `[string, ...number[]]`).
+ * - `unevaluatedItems` or `contains` (without `prefixItems`) infers a uniform
+ * array of that element type.
+ *
+ * @template Prefix - The readonly `prefixItems` tuple, or `undefined`
+ * @template O - The array options, used to infer the element type
* @param options - Optional array schema options
* @returns A schema object for array validation
*
@@ -837,11 +882,25 @@ export interface ArrayOptions extends
* const stringArraySchema = array({ items: string(), minItems: 1 });
* const result = validate(stringArraySchema, ["hello", "world"]);
* // result: { value: ["hello", "world"] }
+ * const parsed: string[] = parse(stringArraySchema, ["hello", "world"]);
+ *
+ * const tupleSchema = array({ prefixItems: [string(), number()] });
+ * const tuple: [string, number] = parse(tupleSchema, ["hello", 42]);
+ *
+ * const restSchema = array({ prefixItems: [string()], items: number() });
+ * const rest: [string, ...number[]] = parse(restSchema, ["hello", 1, 2, 3]);
* ```
*/
-export function array(
- options?: ArrayOptions,
-): SchemaObject<"array", unknown[], unknown[]> {
+export function array<
+ const Prefix extends ReadonlyArray | undefined = undefined,
+ O extends ArrayOptions | undefined = undefined,
+>(
+ options?: O & { prefixItems?: Prefix },
+): SchemaObject<
+ "array",
+ ArrayElementOutput,
+ ArrayElementOutput
+> {
return schema(
{ type: "array", ...options },
{
@@ -996,7 +1055,9 @@ export function array(
}
}
- return issues.length ? { issues } : { value };
+ return issues.length
+ ? { issues }
+ : { value: value as ArrayElementOutput };
},
input: (params) => {
return {
@@ -1062,10 +1123,57 @@ export interface ObjectOptions extends
> {
}
+/**
+ * The inferred output type for an object schema, derived from its `properties`,
+ * `required`, and `additionalProperties`.
+ *
+ * - Keys listed in `required` are required; the remaining declared keys are
+ * optional.
+ * - `additionalProperties` controls extra (undeclared) keys: `false` removes the
+ * index signature, a schema types the extra values, and `true`/absent allows
+ * `unknown` extra values.
+ *
+ * @template Properties - The `properties` record
+ * @template Required - The readonly `required` string tuple
+ * @template AdditionalProperties - The `additionalProperties` option value
+ */
+export type ObjectElementOutput<
+ Properties,
+ Required extends ReadonlyArray,
+ AdditionalProperties,
+> = InferObjectOutput;
+
+/**
+ * Resolves the inferred output type of an {@link object} schema from its
+ * options. Used internally so the `validate` return annotation and the returned
+ * value share the exact same type (avoiding spurious mismatches between
+ * conditionals that differ only in `infer P` vs `infer P | undefined`).
+ *
+ * @template O - The {@link ObjectOptions}
+ * @template Required - The readonly `required` string tuple
+ */
+type ObjectOutputOf<
+ O extends ObjectOptions | undefined,
+ Required extends ReadonlyArray | undefined,
+> = ObjectElementOutput<
+ O extends { properties?: infer P } ? P : never,
+ Required extends ReadonlyArray ? Required : [],
+ O extends { additionalProperties?: infer AP } ? AP : undefined
+>;
+
/**
* Creates an object schema that validates object values.
* Supports constraints for properties, patterns, and additional properties.
*
+ * Type inference uses `properties`, `required`, and `additionalProperties`:
+ * - Keys listed in `required` are required; the remaining declared keys are
+ * optional.
+ * - `additionalProperties: false` disallows extra (undeclared) keys;
+ * `additionalProperties: ` types extra values; `true`/absent allows
+ * `unknown` extra values.
+ *
+ * @template Required - The readonly `required` string tuple, or `undefined`
+ * @template O - The object options, used to infer the output shape
* @param options - Optional object schema options
* @returns A schema object for object validation
*
@@ -1080,15 +1188,33 @@ export interface ObjectOptions extends
* });
* const result = validate(personSchema, { name: "Alice", age: 30 });
* // result: { value: { name: "Alice", age: 30 } }
+ * const parsed: { name: string; age?: number } = parse(personSchema, { name: "Alice" });
+ *
+ * const strict = object({
+ * properties: { name: string() },
+ * required: ["name"],
+ * additionalProperties: false,
+ * });
+ * const strictParsed: { name: string } = parse(strict, { name: "Alice" });
* ```
*/
-export function object(
- options?: ObjectOptions,
-): SchemaObject<"object", object, object> {
+export function object<
+ const Required extends ReadonlyArray | undefined = undefined,
+ O extends ObjectOptions | undefined = undefined,
+>(
+ options?: O & { required?: Required },
+): SchemaObject<
+ "object",
+ ObjectOutputOf,
+ ObjectOutputOf
+> {
return schema(
{ type: "object", ...options },
{
- validate: (value, _opts) => {
+ validate: (
+ value,
+ _opts,
+ ): StandardSchemaV1.Result> => {
if (!isObject(value)) {
return failureResult(msg.invalidType("object", value));
}
@@ -1238,7 +1364,9 @@ export function object(
}
}
- return issues.length ? { issues } : { value };
+ return issues.length ? { issues } : {
+ value: value as ObjectOutputOf,
+ };
},
input: (params) => {
return {
@@ -1290,10 +1418,33 @@ export interface CombinationOptions extends
> {
}
+/**
+ * The inferred output type for a combination schema, derived as the union of
+ * the inferred output types of its `allOf`, `anyOf` and `oneOf` members. When
+ * no members are present the result is `unknown`.
+ *
+ * @template O - The {@link CombinationOptions} passed to the builder
+ */
+export type CombinationElementOutput =
+ O extends {
+ allOf?: infer A;
+ anyOf?: infer B;
+ oneOf?: infer C;
+ } ? InferCombinationOutput<
+ A extends ReadonlyArray ? A : [],
+ B extends ReadonlyArray ? B : [],
+ C extends ReadonlyArray ? C : []
+ >
+ : unknown;
+
/**
* Creates a combination schema that combines multiple schemas.
* Supports allOf, anyOf, oneOf, and not for complex validation logic.
*
+ * When `allOf`, `anyOf` or `oneOf` are provided, the schema's input and output
+ * types are inferred as the union of the member schemas' output types.
+ *
+ * @template O - The combination options, used to infer the output union
* @param options - Optional combination schema options
* @returns A schema object for combination validation
*
@@ -1304,15 +1455,25 @@ export interface CombinationOptions extends
* });
* const result = validate(combinedSchema, "hello world");
* // result: { value: "hello world" }
+ * const parsed: string = parse(combinedSchema, "hello world");
* ```
*/
-export function combination(
- options?: CombinationOptions,
-): SchemaObject<"combination", unknown, unknown> {
+export function combination<
+ O extends CombinationOptions | undefined = undefined,
+>(
+ options?: O,
+): SchemaObject<
+ "combination",
+ CombinationElementOutput,
+ CombinationElementOutput
+> {
return schema(
{ type: "combination", ...options },
{
- validate: (value, _opts) => {
+ validate: (
+ value,
+ _opts,
+ ): StandardSchemaV1.Result> => {
const validateAndCount = (
schemas: StandardSchemaV1 | StandardSchemaV1[],
) => {
@@ -1364,7 +1525,7 @@ export function combination(
return { issues };
}
}
- return { value };
+ return { value: value as CombinationElementOutput };
},
input: (params) => {
return {
diff --git a/validation/mod.ts b/validation/mod.ts
index 05226ab..67cdb1e 100644
--- a/validation/mod.ts
+++ b/validation/mod.ts
@@ -1,5 +1,19 @@
export * from "./validator.ts";
export * from "./json_schema.ts";
+export type {
+ InferArrayHasRest,
+ InferArrayOutput,
+ InferArrayRest,
+ InferArrayTuple,
+ InferCombinationOutput,
+ InferInput,
+ InferMemberOutput,
+ InferMemberOutputRecord,
+ InferObjectAdditionalIndex,
+ InferObjectOutput,
+ InferObjectRequiredKeys,
+ InferOutput,
+} from "./infer.ts";
export {
getStandardJSONSchemaV1Input,
getStandardJSONSchemaV1Output,
diff --git a/validation/validator.ts b/validation/validator.ts
index 4b11f11..d2468a0 100644
--- a/validation/validator.ts
+++ b/validation/validator.ts
@@ -2,6 +2,13 @@ import type { StandardSchemaV1 } from "@standard-schema/spec";
import { SchemaError } from "@standard-schema/utils";
import { stringify } from "./utils.ts";
+/**
+ * Re-export of the inference helpers for convenience. These read the schema's
+ * `~standard.types` field (set by the schema builders in `./json_schema.ts`)
+ * so that the input/output types of any Standard Schema can be extracted.
+ */
+export type { InferInput, InferOutput } from "./infer.ts";
+
/**
* Validates input against a StandardSchema
*
@@ -25,7 +32,7 @@ import { stringify } from "./utils.ts";
*/
export function validateAsync(
schema: S | boolean,
- input: StandardSchemaV1.InferInput | unknown,
+ input: unknown,
options?: Parameters[1],
):
| StandardSchemaV1.Result>
@@ -70,7 +77,7 @@ export function validateAsync(
*/
export function validate(
schema: S | boolean,
- input: StandardSchemaV1.InferInput | unknown,
+ input: unknown,
options?: Parameters[1],
): StandardSchemaV1.Result> {
const result = validateAsync(schema, input, options);
@@ -104,7 +111,7 @@ export function validate(
*/
export async function parseAsync(
schema: S | boolean,
- input: StandardSchemaV1.InferInput | unknown,
+ input: unknown,
options?: Parameters[1],
): Promise> {
let result = validateAsync(schema, input, options);
@@ -144,7 +151,7 @@ export async function parseAsync(
*/
export function parse(
schema: S | boolean,
- input: StandardSchemaV1.InferInput | unknown,
+ input: unknown,
options?: Parameters[1],
): StandardSchemaV1.InferOutput {
const result = validate(schema, input, options);