Add option to generate never for not schemas that accept everything

Merged
TH
4d ago
+117-1

Fixes #1821

not: {} generates any. No value can satisfy that schema, so never describes it better. With the schema from the issue, a union that allows exactly one of two properties comes out with externalId?: any and internalId?: any, so TypeScript accepts objects that set both.

Changing that output can break existing code, so after review it's now behind a new option: emptyNotAsNever in generateApi, --empty-not-as-never on the CLI. It is off by default and generated output doesn't change unless you turn it on.

With it on, not: {} and not: true generate never. true is not in the issue, but it accepts every value just like {}. Any other not still generates any, including annotation-only schemas such as not: { description: "..." }. The changeset is now a minor bump.

What I ran, on Node 22:

  • tests/spec/not-empty-schema has two tests. With the old parser, the option-on test fails at expect(content).toContain("externalId?: never;"). The default-off test passes with both.
  • vitest run: 306 tests in 101 files pass, with no snapshot changes.
  • biome check on the changed files reports one error, organizeImports at types/index.ts:1:1. This PR doesn't touch those lines.

What I did not run: bun run build, or Node 20, 24 and 25.


Summary by cubic

Fixes #1821 by adding an opt-in emptyNotAsNever option (--empty-not-as-never) that generates never instead of any for not: {} and not: true schemas, which accept every value and so can never be satisfied. Previously NotSchemaParser always returned any, letting TypeScript accept objects in the "exactly one of these properties" pattern that set both. The option is off by default, so existing generated output is unchanged unless it's enabled. Other not schemas still generate any because TypeScript cannot express them. Adds a changeset and a spec test.