diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b99bf85b..9ab511321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ - The `brandHandling` should consist of postprocessing functions altering the depiction made by Zod 4; - The `Depicter` type signature changed; - The `optionalPropStyle` option removed from `Integration` class constructor: - - Use `.optional()` to add question mark to the object property; + - Use `.optional()` to add question mark to the object property as well as `undefined` to its type; - Use `.or(z.undefined())` to add `undefined` to the type of the object property; - Reasoning: https://x.com/colinhacks/status/1919292504861491252; - `z.any()` and `z.unknown()` are not optional, details: https://v4.zod.dev/v4/changelog#changes-zunknown-optionality. diff --git a/example/example.client.ts b/example/example.client.ts index d3e9c7c65..61a7d056b 100644 --- a/example/example.client.ts +++ b/example/example.client.ts @@ -1,6 +1,6 @@ type Type1 = { title: string; - features?: Type1[]; + features?: Type1[] | undefined; }; type SomeOf = T[keyof T]; @@ -265,15 +265,15 @@ interface PostV1AvatarRawNegativeResponseVariants { /** get /v1/events/stream */ type GetV1EventsStreamInput = { /** @deprecated for testing error response */ - trigger?: string; + trigger?: string | undefined; }; /** get /v1/events/stream */ type GetV1EventsStreamPositiveVariant1 = { data: number; event: "time"; - id?: string; - retry?: number; + id?: string | undefined; + retry?: number | undefined; }; /** get /v1/events/stream */ diff --git a/express-zod-api/src/typescript-api.ts b/express-zod-api/src/typescript-api.ts index 5b45ef980..a104c4b86 100644 --- a/express-zod-api/src/typescript-api.ts +++ b/express-zod-api/src/typescript-api.ts @@ -142,11 +142,17 @@ export const makeInterfaceProp = ( comment, }: { isOptional?: boolean; isDeprecated?: boolean; comment?: string } = {}, ) => { + const propType = ensureTypeNode(value); const node = f.createPropertySignature( undefined, makePropertyIdentifier(name), isOptional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, - ensureTypeNode(value), + isOptional + ? f.createUnionTypeNode([ + propType, + ensureTypeNode(ts.SyntaxKind.UndefinedKeyword), + ]) + : propType, ); const jsdoc = R.reject(R.isNil, [ isDeprecated ? "@deprecated" : undefined, diff --git a/express-zod-api/tests/__snapshots__/integration.spec.ts.snap b/express-zod-api/tests/__snapshots__/integration.spec.ts.snap index 4e4a2c452..38e79e133 100644 --- a/express-zod-api/tests/__snapshots__/integration.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/integration.spec.ts.snap @@ -282,14 +282,14 @@ exports[`Integration > Should treat optionals the same way as z.infer() by defau /** post /v1/test-with-dashes */ type PostV1TestWithDashesInput = { - opt?: string; + opt?: string | undefined; }; /** post /v1/test-with-dashes */ type PostV1TestWithDashesPositiveVariant1 = { status: "success"; data: { - similar?: number; + similar?: number | undefined; }; }; diff --git a/express-zod-api/tests/__snapshots__/zts.spec.ts.snap b/express-zod-api/tests/__snapshots__/zts.spec.ts.snap index c0035bf4b..ccf5869f3 100644 --- a/express-zod-api/tests/__snapshots__/zts.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/zts.spec.ts.snap @@ -22,14 +22,14 @@ exports[`zod-to-ts > Example > should produce the expected results 1`] = ` any: any; unknown: any; never: any; - optionalString?: string; + optionalString?: string | undefined; nullablePartialObject: { - string?: string; - number?: number; - fixedArrayOfString?: string[]; + string?: string | undefined; + number?: number | undefined; + fixedArrayOfString?: string[] | undefined; object?: { string: string; - }; + } | undefined; } | null; tuple: [ string, @@ -57,7 +57,7 @@ exports[`zod-to-ts > Example > should produce the expected results 1`] = ` set: any; intersection: (string & number) | bigint; promise: any; - optDefaultString?: string; + optDefaultString?: string | undefined; refinedStringWithSomeBullshit: (string | number) & (bigint | null); nativeEnum: "A" | "apple" | "banana" | "cantaloupe" | 5; lazy: SomeType; @@ -127,7 +127,7 @@ exports[`zod-to-ts > Issue #2352: intersection of objects having same prop %# > "{ query: string; } & { - query?: string; + query?: string | undefined; }" `; @@ -135,7 +135,7 @@ exports[`zod-to-ts > Issue #2352: intersection of objects having same prop %# > "{ query: string; } & { - query?: string; + query?: string | undefined; }" `; @@ -272,7 +272,7 @@ exports[`zod-to-ts > z.optional() > Zod 4: does not add undefined to it, unwrap exports[`zod-to-ts > z.optional() > Zod 4: should add question mark only to optional props 1`] = ` "{ - optional?: string; + optional?: string | undefined; required: string; transform: number; or: number | string; @@ -280,9 +280,9 @@ exports[`zod-to-ts > z.optional() > Zod 4: should add question mark only to opti string, number, { - optional?: string; + optional?: string | undefined; required: string; } - ]; + ] | undefined; }" `; diff --git a/express-zod-api/tests/env.spec.ts b/express-zod-api/tests/env.spec.ts index 9cc682e1c..9bea65916 100644 --- a/express-zod-api/tests/env.spec.ts +++ b/express-zod-api/tests/env.spec.ts @@ -122,6 +122,18 @@ describe("Environment checks", () => { expect(schema._zod.def.shape.three._zod.optionality).toBe("defaulted"); /** @link https://github.com/colinhacks/zod/issues/4322 */ expect(schema._zod.def.shape.four._zod.optionality).not.toBe("optional"); // <— undefined + expectTypeOf>().toEqualTypeOf<{ + one: boolean; + two?: boolean | undefined; + three?: boolean | undefined; + four: boolean | undefined; + }>(); + expectTypeOf>().toEqualTypeOf<{ + one: boolean; + two?: boolean | undefined; + three: boolean; + four: boolean; + }>(); }); test("coerce is safe for nullable and optional", () => {