From 12264238d669998e4ded4b9e2695c56d37150edc Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:21:15 +0800 Subject: [PATCH] fix(types): zod example teaches the Zod 4 `.issues` accessor, and `examples/` is type-checked (#2919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ZodError.errors` was removed in Zod 4 (the repo is on 4.4.3). `packages/types/examples/zod-validation-example.ts` read `.errors` in seven places, so every `console.error` printed `undefined` and the last one — `invalidButtonResult.error.errors.length` — threw `TypeError: Cannot read properties of undefined (reading 'length')`, killing the example before its summary. Same bug and same cause as the `objectui validate` fix in #2919. `src/zod/README.md` documented the same dead accessor plus a Zod 3 issue shape; both corrected against what 4.4.3 emits (`invalid_value` + `values`, and "Invalid option: expected one of ..."). The example was invisible to CI, so the swap alone would let this rot again. `packages/types` type-checks a project whose `include` is `["src/**/*"]`, so `examples/` sat outside it — the `"examples"` entry in `exclude` was belt-and-braces, and deleting it alone would have changed nothing. Examples cannot join that project either: it is the package build (`tsc` -> `dist`) with `rootDir: "./src"`, `composite` and `declaration`. Added an emit-free `tsconfig.examples.json` and chained it from `type-check`. The example now imports `../src/zod/index.zod` instead of `../dist/zod/index.zod.js`, matching its three sibling example files, so the check needs no prior build. Verified the gate has teeth: restoring `.errors` fails with seven `TS2339: Property 'errors' does not exist on type 'ZodError<...>'`. Co-Authored-By: Claude --- .changeset/zod4-issues-types-examples.md | 41 +++++++++++++++++++ .../types/examples/zod-validation-example.ts | 20 +++++---- packages/types/package.json | 2 +- packages/types/src/zod/README.md | 10 +++-- packages/types/tsconfig.examples.json | 19 +++++++++ 5 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 .changeset/zod4-issues-types-examples.md create mode 100644 packages/types/tsconfig.examples.json diff --git a/.changeset/zod4-issues-types-examples.md b/.changeset/zod4-issues-types-examples.md new file mode 100644 index 0000000000..d4d8ea7e73 --- /dev/null +++ b/.changeset/zod4-issues-types-examples.md @@ -0,0 +1,41 @@ +--- +"@object-ui/types": patch +--- + +fix(types): zod-validation example and zod README teach the Zod 4 `.issues` accessor, and `examples/` is now type-checked + +`ZodError.errors` was removed in Zod 4 (the repo is on 4.4.3). The +`packages/types/examples/zod-validation-example.ts` documentation example read +`.errors` in seven places, so every `console.error` printed `undefined` and the +last one — `invalidButtonResult.error.errors.length` — threw +`TypeError: Cannot read properties of undefined (reading 'length')`, killing the +example before its summary. Same bug, same cause as the `objectui validate` fix +in #2919; now reads `.issues`. + +`src/zod/README.md` documented the same dead accessor plus a Zod 3 issue shape +(`code: 'invalid_enum_value'`, `"Invalid enum value. Expected …"`). Both were +corrected against what 4.4.3 actually emits: `code: 'invalid_value'` with a +`values` array and `'Invalid option: expected one of …'`. + +**The example was invisible to CI, so the swap alone would let this rot again.** +`packages/types` type-checks with `tsc --noEmit` over a project whose `include` +is `["src/**/*"]` — `examples/` was outside it (the `"examples"` entry in +`exclude` was belt-and-braces; deleting it alone would have changed nothing). +Examples cannot simply join that project either: it is the package build +(`tsc` → `dist`) with `rootDir: "./src"`, `composite` and `declaration`, so +example files are both outside `rootDir` and would emit into `dist`. + +Added `packages/types/tsconfig.examples.json` — an emit-free project covering +`examples/**/*.ts` — and chained it: `"type-check": "tsc --noEmit && tsc -p +tsconfig.examples.json"`. The example also now imports from `../src/zod/index.zod` +rather than `../dist/zod/index.zod.js`, matching its three sibling example files +(`dashboard.ts`, `login-form.ts`, `rest-data-source.ts`, all on `../src/index`) +so the check needs no prior build. + +Verified the gate has teeth rather than trusting the green: restoring `.errors` +makes `tsc -p tsconfig.examples.json` fail with seven +`TS2339: Property 'errors' does not exist on type 'ZodError<…>'`. The example +also runs clean end-to-end again, printing `Expected validation errors: 2` +where it previously threw. + +No runtime or published-type change: `examples/` is not in the package's `files`. diff --git a/packages/types/examples/zod-validation-example.ts b/packages/types/examples/zod-validation-example.ts index b8674c7207..a0f28515fe 100644 --- a/packages/types/examples/zod-validation-example.ts +++ b/packages/types/examples/zod-validation-example.ts @@ -19,7 +19,11 @@ import { CardSchema, DataTableSchema, KanbanSchema, -} from '../dist/zod/index.zod.js'; +} from '../src/zod/index.zod'; + +// The failure accessor below is `error.issues`. Zod 4 removed the `.errors` +// alias, so `.errors` reads `undefined` and any `.length`/`.forEach` on it +// throws — see the same bug fixed in `objectui validate` (#2919). // Example 1: Validate a Button component const buttonExample = { @@ -33,7 +37,7 @@ const buttonExample = { const buttonResult = ButtonSchema.safeParse(buttonExample); console.log('Button validation:', buttonResult.success ? 'PASSED ✓' : 'FAILED ✗'); if (!buttonResult.success) { - console.error('Button errors:', buttonResult.error.errors); + console.error('Button errors:', buttonResult.error.issues); } // Example 2: Validate an Input component @@ -49,7 +53,7 @@ const inputExample = { const inputResult = InputSchema.safeParse(inputExample); console.log('Input validation:', inputResult.success ? 'PASSED ✓' : 'FAILED ✗'); if (!inputResult.success) { - console.error('Input errors:', inputResult.error.errors); + console.error('Input errors:', inputResult.error.issues); } // Example 3: Validate a Form component @@ -77,7 +81,7 @@ const formExample = { const formResult = FormSchema.safeParse(formExample); console.log('Form validation:', formResult.success ? 'PASSED ✓' : 'FAILED ✗'); if (!formResult.success) { - console.error('Form errors:', formResult.error.errors); + console.error('Form errors:', formResult.error.issues); } // Example 4: Validate a Card component @@ -97,7 +101,7 @@ const cardExample = { const cardResult = CardSchema.safeParse(cardExample); console.log('Card validation:', cardResult.success ? 'PASSED ✓' : 'FAILED ✗'); if (!cardResult.success) { - console.error('Card errors:', cardResult.error.errors); + console.error('Card errors:', cardResult.error.issues); } // Example 5: Validate a DataTable component @@ -124,7 +128,7 @@ const dataTableExample = { const dataTableResult = DataTableSchema.safeParse(dataTableExample); console.log('DataTable validation:', dataTableResult.success ? 'PASSED ✓' : 'FAILED ✗'); if (!dataTableResult.success) { - console.error('DataTable errors:', dataTableResult.error.errors); + console.error('DataTable errors:', dataTableResult.error.issues); } // Example 6: Validate a Kanban component @@ -160,7 +164,7 @@ const kanbanExample = { const kanbanResult = KanbanSchema.safeParse(kanbanExample); console.log('Kanban validation:', kanbanResult.success ? 'PASSED ✓' : 'FAILED ✗'); if (!kanbanResult.success) { - console.error('Kanban errors:', kanbanResult.error.errors); + console.error('Kanban errors:', kanbanResult.error.issues); } // Example 7: Test validation errors @@ -175,7 +179,7 @@ const invalidButton = { const invalidButtonResult = ButtonSchema.safeParse(invalidButton); console.log('Invalid button validation:', invalidButtonResult.success ? 'PASSED (unexpected)' : 'FAILED (expected) ✓'); if (!invalidButtonResult.success) { - console.log('Expected validation errors:', invalidButtonResult.error.errors.length); + console.log('Expected validation errors:', invalidButtonResult.error.issues.length); } // Summary diff --git a/packages/types/package.json b/packages/types/package.json index 5ee500caf4..580ecfcc56 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -64,7 +64,7 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", - "type-check": "tsc --noEmit", + "type-check": "tsc --noEmit && tsc -p tsconfig.examples.json", "lint": "eslint ." }, "keywords": [ diff --git a/packages/types/src/zod/README.md b/packages/types/src/zod/README.md index 5f88782780..05ed8b40f3 100644 --- a/packages/types/src/zod/README.md +++ b/packages/types/src/zod/README.md @@ -243,14 +243,18 @@ const result = ButtonSchema.safeParse({ variant: 'invalid-variant' }); -// result.error.errors: +// result.error.issues: // [ // { -// code: 'invalid_enum_value', +// code: 'invalid_value', +// values: ['default', 'secondary', ...], // path: ['variant'], -// message: "Invalid enum value. Expected 'default' | 'secondary' | ...", +// message: 'Invalid option: expected one of "default"|"secondary"|...', // } // ] +// +// Note: the accessor is `.issues`. Zod 4 removed the `.errors` alias, so +// `.errors` reads `undefined` rather than throwing at the access itself. ``` ### Nested Validation diff --git a/packages/types/tsconfig.examples.json b/packages/types/tsconfig.examples.json new file mode 100644 index 0000000000..ad0ebe37c8 --- /dev/null +++ b/packages/types/tsconfig.examples.json @@ -0,0 +1,19 @@ +{ + // `examples/` is documentation that consumers copy from, so it has to compile + // against the same sources it teaches. It cannot live in tsconfig.json: that + // project is the package build (`tsc` -> dist) with "rootDir": "./src", + // "composite" and "declaration", so example files are both outside rootDir and + // would emit into dist. Note that deleting "examples" from that file's + // "exclude" would change nothing on its own — its "include" is already scoped + // to "src/**/*". Hence a separate, emit-free project, chained from the + // package's `type-check` script. + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "lib": ["ES2020", "DOM"], + // Same reason as tsconfig.json: drop the root tsconfig's source-tree paths + // so @objectstack/spec resolves through the workspace dependency. + "paths": {} + }, + "include": ["examples/**/*.ts"] +}