Skip to content

Runtime behavior of optional/undefinedable doesn’t match TS with exactOptionalPropertyTypes #983

@andersk

Description

@andersk

#385 was resolved by adding v.undefinedable with different typing from v.optional:

import * as v from "valibot";

const optional_schema = v.object({ field: v.optional(v.number()) });
type OptionalOutput = v.InferOutput<typeof optional_schema>;
// { field?: number }

const undefinedable_schema = v.object({ field: v.undefinedable(v.number()) });
type UndefinedableOutput = v.InferOutput<typeof undefinedable_schema>;
// { field: number | undefined }

However, this typing is inconsistent with the runtime behavior: at runtime, both schemas accept and return both of {} and { field: undefined }. This means that both schemas break type safety with tsc --strict --exactOptionalPropertyTypes.

const a = v.parse(optional_schema, { field: undefined });
if ("field" in a) { // narrows a.field to string
  const n: number = a.field; // oops, this is undefined
}

const b = v.parse(
  v.union([undefinedable_schema, v.object({ bogus: v.string() })]),
  {}
);
if (!("field" in b)) { // narrows b to { bogus: string }
  const s: string = b.bogus; // oops, this is undefined
}

Either the types should be adjusted to match the runtime behavior, or the runtime behavior should be adjusted to match the types.

Zod does the former:

import { z } from "zod";

const optional_schema = z.object({ field: z.optional(z.number()) });
type OptionalOutput = z.output<typeof optional_schema>;
// { field?: number | undefined }

Metadata

Metadata

Assignees

Labels

fixA smaller enhancement or bug fixintendedThe behavior is expectedpriorityThis has priority

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions