Skip to content
Merged
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
41 changes: 41 additions & 0 deletions .changeset/zod4-issues-types-examples.md
Original file line number Diff line number Diff line change
@@ -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`.
20 changes: 12 additions & 8 deletions packages/types/examples/zod-validation-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
10 changes: 7 additions & 3 deletions packages/types/src/zod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions packages/types/tsconfig.examples.json
Original file line number Diff line number Diff line change
@@ -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"]
}
Loading