Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Optional object properties are not equivalent to required properties with undefined values #3817

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`sveltekit-superforms`](https://github.com/ciscoheat/sveltekit-superforms): Supercharged form library for SvelteKit with Zod validation.
- [`mobx-zod-form`](https://github.com/MonoidDev/mobx-zod-form): Data-first form builder based on MobX & Zod.
- [`@vee-validate/zod`](https://github.com/logaretm/vee-validate/tree/main/packages/zod): Form library for Vue.js with Zod schema validation.
- [`zod-form-renderer`](https://github.com/thepeaklab/zod-form-renderer): Auto-infer form fields from zod schema and render them with react-hook-form with E2E type safety.

#### Zod to X

Expand All @@ -505,6 +506,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`zod-openapi`](https://github.com/samchungy/zod-openapi): Create full OpenAPI v3.x documentation from Zod schemas.
- [`fastify-zod-openapi`](https://github.com/samchungy/fastify-zod-openapi): Fastify type provider, validation, serialization and @fastify/swagger support for Zod schemas.
- [`typeschema`](https://typeschema.com/): Universal adapter for schema validation.
- [`zodex`](https://github.com/commonbaseapp/zodex): (De)serialization for zod schemas

#### X to Zod

Expand Down Expand Up @@ -538,11 +540,13 @@ There are a growing number of tools that are built atop or support Zod natively!

- [`freerstore`](https://github.com/JacobWeisenburger/freerstore): Firestore cost optimizer.
- [`slonik`](https://github.com/gajus/slonik/tree/gajus/add-zod-validation-backwards-compatible#runtime-validation-and-static-type-inference): Node.js Postgres client with strong Zod integration.
- [`schemql`](https://github.com/a2lix/schemql): Enhances your SQL workflow by combining raw SQL with targeted type safety and schema validation.
- [`soly`](https://github.com/mdbetancourt/soly): Create CLI applications with zod.
- [`pastel`](https://github.com/vadimdemedes/pastel): Create CLI applications with react, zod, and ink.
- [`zod-xlsx`](https://github.com/sidwebworks/zod-xlsx): A xlsx based resource validator using Zod schemas.
- [`znv`](https://github.com/lostfictions/znv): Type-safe environment parsing and validation for Node.js with Zod schemas.
- [`zod-config`](https://github.com/alexmarqs/zod-config): Load configurations across multiple sources with flexible adapters, ensuring type safety with Zod.
- [`unplugin-environment`](https://github.com/r17x/js/tree/main/packages/unplugin-environment#readme): A plugin for loading enviroment variables safely with schema validation, simple with virtual module, type-safe with intellisense, and better DX 🔥 🚀 👷. Powered by Zod.

#### Utilities for Zod

Expand Down
4 changes: 2 additions & 2 deletions deno/lib/__tests__/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ test("inferred type for unknown/any keys", () => {
myType,
{
anyOptional?: any;
anyRequired?: any;
anyRequired: any;
unknownOptional?: unknown;
unknownRequired?: unknown;
unknownRequired: unknown;
}
>(true);
});
Expand Down
30 changes: 22 additions & 8 deletions deno/lib/helpers/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ZodDefault, ZodOptional, ZodRawShape, ZodTypeAny } from "../types.ts";

export namespace util {
type AssertEqual<T, U> = (<V>() => V extends T ? 1 : 2) extends <
V
Expand Down Expand Up @@ -101,17 +103,29 @@ export namespace objectUtil {
[k in Exclude<keyof U, keyof V>]: U[k];
} & V;

type optionalKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? k : never;
type ObjectField = '_output' | '_input';

type optionalKeys<T extends ZodRawShape, I extends ObjectField> = {
[k in keyof T]: T[k] extends ZodOptional<ZodTypeAny>
? k
: T[k] extends ZodDefault<ZodTypeAny>
? I extends '_output' ? never : k
: never;
}[keyof T];
type requiredKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? never : k;

type requiredKeys<T extends ZodRawShape, I extends ObjectField> = {
[k in keyof T]: T[k] extends ZodOptional<ZodTypeAny>
? never
: T[k] extends ZodDefault<ZodTypeAny>
? I extends '_output' ? k : never
: k;
}[keyof T];
export type addQuestionMarks<T extends object, _O = any> = {
[K in requiredKeys<T>]: T[K];

export type addQuestionMarks<Shape extends ZodRawShape, I extends '_output' | '_input', _O = any> = {
[K in requiredKeys<Shape, I>]: Shape[K][I];
} & {
[K in optionalKeys<T>]?: T[K];
} & { [k in keyof T]?: unknown };
[K in optionalKeys<Shape, I>]?: Shape[K][I] | undefined;
} & { [k in keyof Shape]?: unknown };

export type identity<T> = T;
export type flatten<T> = identity<{ [k in keyof T]: T[k] }>;
Expand Down
14 changes: 2 additions & 12 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2330,27 +2330,17 @@ export type objectOutputType<
Shape extends ZodRawShape,
Catchall extends ZodTypeAny,
UnknownKeys extends UnknownKeysParam = UnknownKeysParam
> = objectUtil.flatten<
objectUtil.addQuestionMarks<baseObjectOutputType<Shape>>
> &
> = objectUtil.flatten<objectUtil.addQuestionMarks<Shape, "_output">> &
CatchallOutput<Catchall> &
PassthroughType<UnknownKeys>;

export type baseObjectOutputType<Shape extends ZodRawShape> = {
[k in keyof Shape]: Shape[k]["_output"];
};

export type objectInputType<
Shape extends ZodRawShape,
Catchall extends ZodTypeAny,
UnknownKeys extends UnknownKeysParam = UnknownKeysParam
> = objectUtil.flatten<baseObjectInputType<Shape>> &
> = objectUtil.flatten<objectUtil.addQuestionMarks<Shape, "_input">> &
CatchallInput<Catchall> &
PassthroughType<UnknownKeys>;
export type baseObjectInputType<Shape extends ZodRawShape> =
objectUtil.addQuestionMarks<{
[k in keyof Shape]: Shape[k]["_input"];
}>;

export type CatchallOutput<T extends ZodType> = ZodType extends T
? unknown
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,9 @@ test("inferred type for unknown/any keys", () => {
myType,
{
anyOptional?: any;
anyRequired?: any;
anyRequired: any;
unknownOptional?: unknown;
unknownRequired?: unknown;
unknownRequired: unknown;
}
>(true);
});
Expand Down
30 changes: 22 additions & 8 deletions src/helpers/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ZodDefault, ZodOptional, ZodRawShape, ZodTypeAny } from "../types";

export namespace util {
type AssertEqual<T, U> = (<V>() => V extends T ? 1 : 2) extends <
V
Expand Down Expand Up @@ -101,17 +103,29 @@ export namespace objectUtil {
[k in Exclude<keyof U, keyof V>]: U[k];
} & V;

type optionalKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? k : never;
type ObjectField = '_output' | '_input';

type optionalKeys<T extends ZodRawShape, I extends ObjectField> = {
[k in keyof T]: T[k] extends ZodOptional<ZodTypeAny>
? k
: T[k] extends ZodDefault<ZodTypeAny>
? I extends '_output' ? never : k
: never;
}[keyof T];
type requiredKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? never : k;

type requiredKeys<T extends ZodRawShape, I extends ObjectField> = {
[k in keyof T]: T[k] extends ZodOptional<ZodTypeAny>
? never
: T[k] extends ZodDefault<ZodTypeAny>
? I extends '_output' ? k : never
: k;
}[keyof T];
export type addQuestionMarks<T extends object, _O = any> = {
[K in requiredKeys<T>]: T[K];

export type addQuestionMarks<Shape extends ZodRawShape, I extends '_output' | '_input', _O = any> = {
[K in requiredKeys<Shape, I>]: Shape[K][I];
} & {
[K in optionalKeys<T>]?: T[K];
} & { [k in keyof T]?: unknown };
[K in optionalKeys<Shape, I>]?: Shape[K][I] | undefined;
} & { [k in keyof Shape]?: unknown };

export type identity<T> = T;
export type flatten<T> = identity<{ [k in keyof T]: T[k] }>;
Expand Down
14 changes: 2 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2330,27 +2330,17 @@ export type objectOutputType<
Shape extends ZodRawShape,
Catchall extends ZodTypeAny,
UnknownKeys extends UnknownKeysParam = UnknownKeysParam
> = objectUtil.flatten<
objectUtil.addQuestionMarks<baseObjectOutputType<Shape>>
> &
> = objectUtil.flatten<objectUtil.addQuestionMarks<Shape, "_output">> &
CatchallOutput<Catchall> &
PassthroughType<UnknownKeys>;

export type baseObjectOutputType<Shape extends ZodRawShape> = {
[k in keyof Shape]: Shape[k]["_output"];
};

export type objectInputType<
Shape extends ZodRawShape,
Catchall extends ZodTypeAny,
UnknownKeys extends UnknownKeysParam = UnknownKeysParam
> = objectUtil.flatten<baseObjectInputType<Shape>> &
> = objectUtil.flatten<objectUtil.addQuestionMarks<Shape, "_input">> &
CatchallInput<Catchall> &
PassthroughType<UnknownKeys>;
export type baseObjectInputType<Shape extends ZodRawShape> =
objectUtil.addQuestionMarks<{
[k in keyof Shape]: Shape[k]["_input"];
}>;

export type CatchallOutput<T extends ZodType> = ZodType extends T
? unknown
Expand Down