@@ -38,7 +38,7 @@ export interface TypeDefination<T, O, D = never> extends Rules {
3838 * is called (fresh value per validate) except on `function` schemas,
3939 * where the default IS the fn. */
4040 default ?: D | ( ( ) => D ) ;
41- /** When true, `undefined` passes straight through unvalidated. */
41+ /** When true, `undefined` and `null` pass straight through unvalidated. */
4242 optional ?: boolean ;
4343 transform ?: ( value : any ) => O ;
4444 /** Opaque plugin attributes - ignored by validate / Infer*. */
@@ -52,8 +52,9 @@ export type TypeOptions<T, O> = {
5252} ;
5353
5454/**
55- * `optional` widens the output; `default` keeps it narrow because a value
56- * is always produced. Declaring both means optional to send, never absent.
55+ * `optional` widens the output with `| undefined | null`; `default` keeps
56+ * it narrow because a value is always produced. Declaring both means
57+ * optional to send, never absent.
5758 *
5859 * Helpers select among these via option-shape overloads rather than `Opt` /
5960 * `D` type parameters: providing a partial type argument (e.g.
@@ -62,7 +63,7 @@ export type TypeOptions<T, O> = {
6263 */
6364type OutOf < O , D , Opt > = [ Opt ] extends [ true ]
6465 ? [ D ] extends [ never ]
65- ? O | undefined
66+ ? O | undefined | null
6667 : O
6768 : O ;
6869
@@ -71,15 +72,15 @@ type OutOf<O, D, Opt> = [Opt] extends [true]
7172 * `TypeDefination<any, infer O, …>` drops `| undefined` because `output?`
7273 * is optional and TypeScript attributes the undefined to the property.
7374 * When the third type arg is `undefined` (optional, no default), put
74- * `| undefined` back so handlers see the same absence validate produces
75- * at runtime.
75+ * `| undefined | null ` back so handlers see the same absence validate
76+ * produces at runtime.
7677 */
7778type OutputOf < F > =
7879 F extends TypeDefination < any , infer O , infer D >
7980 ? [ D ] extends [ never ]
8081 ? O
8182 : undefined extends D
82- ? O | undefined
83+ ? O | undefined | null
8384 : O
8485 : never ;
8586
@@ -256,20 +257,23 @@ type FieldIn<F> = F extends { $var: true; schema?: infer S }
256257 ? InferArgs < NonNullable < S > >
257258 : F extends { $fnSchema : { input ?: infer FI ; output ?: infer FO } }
258259 ? SchemaFnIn < FI , FO > & FnVarBrand < FI >
259- : F extends TypeDefination < infer T , any , any >
260- ? T
260+ : F extends TypeDefination < infer T , any , infer D >
261+ ? undefined extends D
262+ ? T | null
263+ : T
261264 : F extends Record < string , unknown >
262265 ? ArgsShape < F >
263266 : F ;
264267
265268/**
266269 * `optional: true` on `v.fn.type` widens the same way a type's `optional`
267- * does: absent without a default means the value may be `undefined`.
270+ * does: absent without a default means the value may be `undefined` or
271+ * `null`.
268272 */
269273type FnSchemaOut < F , Fn > = F extends { optional : true }
270274 ? F extends { default : infer _D }
271275 ? Fn
272- : Fn | undefined
276+ : Fn | undefined | null
273277 : Fn ;
274278
275279/**
@@ -331,8 +335,10 @@ export type InferArgs<I> = I extends { $var: true; schema?: infer S }
331335 ? SchemaFnIn < FI , FO > & FnVarBrand < FI >
332336 : I extends readonly unknown [ ]
333337 ? { - readonly [ K in keyof I ] : InferArgs < I [ K ] > }
334- : I extends TypeDefination < infer T , any , any >
335- ? T
338+ : I extends TypeDefination < infer T , any , infer D >
339+ ? undefined extends D
340+ ? T | null
341+ : T
336342 : ArgsShape < I > ;
337343
338344export const isType = ( value : any ) : value is TypeDefination < any , any > =>
@@ -542,11 +548,12 @@ export const validate = (
542548 value : unknown ,
543549 path : string ,
544550) : any => {
545- // `undefined` falls back to the declared default before anything else,
546- // then to `optional`, which passes it through untouched. A shaped
547- // object with no default treats omit as `{}` so all-optional fields
548- // can be left off the call without a dummy payload.
549- if ( value === undefined ) {
551+ // `undefined` / (when optional) `null` fall back to the declared
552+ // default before anything else, then to `optional`, which passes the
553+ // absence through untouched. A shaped object with no default treats
554+ // omit as `{}` so all-optional fields can be left off the call without
555+ // a dummy payload. Non-optional `null` falls through to the type check.
556+ if ( value === undefined || ( value === null && def . optional ) ) {
550557 if ( def . default !== undefined ) {
551558 // Factories produce a fresh value each time - needed for Date /
552559 // object / array defaults. Skip on `function` schemas: there the
@@ -555,7 +562,7 @@ export const validate = (
555562 typeof def . default === "function" && def . name !== "function"
556563 ? ( def . default as ( ) => unknown ) ( )
557564 : def . default ;
558- } else if ( def . optional ) return undefined ;
565+ } else if ( def . optional ) return value ;
559566 else if ( def . name === "object" && def . shape !== undefined ) value = { } ;
560567 }
561568 // A var used as an input field validates against its own schema. An
@@ -881,7 +888,7 @@ type ArrayFn = {
881888 (
882889 element ?: undefined ,
883890 options ?: ArrayOptions < undefined , any [ ] > & { optional : true } ,
884- ) : TypeDefination < any [ ] , any [ ] | undefined , undefined > ;
891+ ) : TypeDefination < any [ ] , any [ ] | undefined | null , undefined > ;
885892 (
886893 element ?: undefined ,
887894 options ?: ArrayOptions < undefined , any [ ] > & {
0 commit comments