Skip to content

fix(schema): reject Infinity and -Infinity from number by default - #1703

Closed
thecotne wants to merge 1 commit into
arktypeio:mainfrom
thecotne:fix-number-reject-infinity
Closed

thecotne wants to merge 1 commit into
arktypeio:mainfrom
thecotne:fix-number-reject-infinity

Conversation

@thecotne

Copy link
Copy Markdown
Contributor

Closes #1598

This implements the design @ssalbdivad described when closing #1605 (comment):

Rather than adding a separate keyword, we're going to make number reject Infinity by default in an upcoming release (the same way it already rejects NaN), with numberAllowsInfinity as a config to opt back in.

Behavior change

number now rejects Infinity and -Infinity by default. This changes default behavior: values that validated before are now errors, including through ranges, since number > 0 used to accept Infinity.

type.number(Infinity) // ArkErrors: must be a number (was Infinity)
type("number > 0")(Infinity) // ArkErrors: must be a number (was Infinity)
type("number | null")(-Infinity) // ArkErrors: must be a number (was -Infinity)

Opting out

numberAllowsInfinity mirrors numberAllowsNaN:

  • Global config, set from "arktype/config" before importing arktype: configure({ numberAllowsInfinity: true }). Like numberAllowsNaN, it's a global-only option (added to GlobalOnlyConfigOptionName), so scopes and types can't set it.
  • Schema level: rootSchema({ domain: "number", numberAllowsInfinity: true }). Like numberAllowsNaN, it throws for other domains.
  • To accept the values explicitly, keep using the existing keywords: "number | number.Infinity", "number | number.NegativeInfinity".

Details

  • Errors: the actual value is reported as Infinity or -Infinity, the way NaN is. As with NaN, this also applies to other domains' errors, e.g. type("string")(Infinity) now ends with (was Infinity) instead of (was a number).
  • Expression: a domain with numberAllowsInfinity prints as number | Infinity | -Infinity. With both options it prints number | NaN | Infinity | -Infinity.
  • Intersections: intersecting two number domains only allows NaN or Infinity if both sides do.
  • Keywords: number.Infinity and number.NegativeInfinity are unchanged. number | number.Infinity now stays a union (number | Infinity). number & number.Infinity is now unsatisfiable, just like number & number.NaN. number.safe and number.epoch already rejected ±Infinity through their bounds, but now report must be a number (was Infinity), matching NaN. number.integer does the same.
  • Unions: a union discriminated by typeof used to drop the number domain check entirely (case "number": return true). That's why number | null accepted NaN in compiled mode while jitless rejected it. pruneDiscriminant now keeps a number domain that excludes NaN or Infinity, so both are rejected in number | null, { a: number } | { a: string }, and so on. This also fixes Unions discriminated by typeof accept NaN for number (e.g. number | null) #1700, which has its own smaller PR, fix(schema): keep the NaN check when a union discriminates on typeof #1702, touching the same lines. Whichever merges second needs a small rebase: keep this PR's pruneDiscriminant condition, and keep both sets of new tests in number.test.ts.
  • JSON Schema: output is unchanged ({ "type": "number" }). JSON can't represent Infinity, and numberAllowsNaN has no JSON Schema handling either.
  • fast-check: @ark/fast-check now passes noDefaultInfinity: !numberAllowsInfinity, next to the existing noNaN, so number arbitraries don't generate values number would reject.
  • Docs: I updated the configuration page (example and global-only callout), added a note to the number section of Primitives, and regenerated llms.txt.

Tests

  • In keywords/number.test.ts:
    • ±Infinity is rejected by number, number > 0, number < 0 and number.integer, and by the discriminated unions number | null and { a: number } | { a: string }, with JIT and with jitless: true.
    • numberAllowsInfinity works on schemas, alone and combined with numberAllowsNaN, including expressions and intersections.
    • numberAllowsInfinity on a non-number domain throws.
    • number | number.Infinity and number | number.Infinity | number.NegativeInfinity | null accept exactly those values.
    • JSON Schema output is unchanged.
  • In config.test.ts: global numberAllowsInfinity, mirroring the numberAllowsNaN test.
  • In integration/simpleConfig.ts and testSimpleConfig.ts: numberAllowsInfinity: true is applied to built-in keywords.
  • Updated snapshots: the number.safe Infinity messages, and the discriminant snapshots in discrimination.test.ts and match.test.ts, whose "number" case now keeps its domain check.

Checklist:

  • Code is up-to-date with the main branch (based on c34bf69)
  • You've successfully run pnpm prChecks locally
  • There are new or updated unit tests validating the change

Suggested changelog entry

Recent contributor PRs leave CHANGELOG.md to the release PRs, so this one doesn't edit it. Here's a suggested entry:

### Reject `Infinity` from `number` by default

`number` now rejects `Infinity` and `-Infinity` the same way it already rejects `NaN` (#1598):

```ts
// ArkErrors: must be a number (was Infinity)
type.number(Infinity)
```

This is a change in default behavior. To accept them again, use the new global `numberAllowsInfinity` option:

```ts title="config.ts"
import { configure } from "arktype/config"

configure({ numberAllowsInfinity: true })
```

To accept them for specific types, reference `number.Infinity` or `number.NegativeInfinity`, e.g. `"number | number.Infinity"`.

`number` branches of unions discriminated by `typeof` (like `number | null`) now also reject `NaN`, which they previously accepted.

`number` now rejects `Infinity` and `-Infinity` the same way it already
rejects `NaN`, and reports them as the actual value (e.g. "must be a
number (was Infinity)"). This changes default behavior: values that were
accepted before, including by ranges like `number > 0`, are now rejected.

The global `numberAllowsInfinity` config, or `numberAllowsInfinity: true`
on a number domain schema, opts back in, mirroring `numberAllowsNaN`.
`number.Infinity` and `number.NegativeInfinity` still match exactly those
values and can be unioned with `number`.

Discriminating a union on `typeof` no longer prunes a number domain that
excludes NaN or Infinity, so `number | null` rejects them as well when
compiled.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No correctness issues found — one generated file needs regenerating.

Reviewed changes

  • Domain numberAllowsInfinity — ark/schema/roots/domain.ts adds the option, excludes ±Infinity from number by default in both traversal and compiled conditions, and reports Infinity/-Infinity in actual.
  • Intersection semantics — intersecting two number domains now only allows NaN/Infinity if both sides do, and falls back to a fresh number domain when neither side is a subset. The subset tests are correct.
  • Discriminant pruning — pruneDiscriminant now keeps a number domain that excludes NaN/Infinity, so typeof-discriminated unions like number | null and { a: number } | { a: string } reject both in JIT and jitless mode (also fixes #1700). Verified runtime behavior is unchanged for the reworked match snapshot.
  • Config plumbing — numberAllowsInfinity added to ArkSchemaConfig, defaultConfig, and GlobalOnlyConfigOptionName (global-only, matching numberAllowsNaN).
  • fast-check — number arbitraries pass noDefaultInfinity: !numberAllowsInfinity; noDefaultInfinity is present in the pinned fast-check@3.23.2.
  • Docs/tests — primitives/configuration docs and llms.txt updated; new tests cover rejection, opt-out, intersections, and jitless. I ran the touched suites (number, config, match, discrimination, schema, testSimpleConfig) — all pass.

ℹ️ Stale generated @ark/schema DTS bundle

ark/docs/components/dts/schema.ts is a committed autogenerated bundle (from ark/repo/dtsGen.ts) whose inlined ArkSchemaConfig and GlobalOnlyConfigOptionName still list numberAllowsNaN but not numberAllowsInfinity. It feeds the playground's TypeScript server, and the docs deploy workflow runs only pnpm buildDocs (next build), which does not regenerate it — so the published types will lag until it is rebuilt. A prior change to config.ts (fe481db3) regenerated this file in the same PR.

Technical details
# Regenerate `ark/docs/components/dts/schema.ts`

## Affected sites
- `ark/docs/components/dts/schema.ts` — autogenerated bundle still declares `numberAllowsNaN?: boolean` and the `GlobalOnlyConfigOptionName` union without `numberAllowsInfinity`.

## Required outcome
- The committed bundle reflects the new `numberAllowsInfinity` field and `GlobalOnlyConfigOptionName` member.

## Suggested approach
- Run `pnpm build` (the `@ark/schema` build invokes `dtsGen()`) and commit the regenerated `ark/docs/components/dts/schema.ts`.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@ssalbdivad

ssalbdivad commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

thanks @thecotne, really appreciate you putting this together! since it changes what number accepts it's a breaking change, so we're holding it for our next major, which already has the same numberAllowsInfinity opt-in. closing in favor of that, #1598 will close when it ships

@ssalbdivad ssalbdivad closed this Oct 20, 2026
@github-project-automation github-project-automation Bot moved this from To do to Done (merged or closed) in arktypeio Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done (merged or closed)

Development

Successfully merging this pull request may close these issues.

Make "number" reject Infinity, or make it easy to parse only finite numbers Unions discriminated by typeof accept NaN for number (e.g. number | null)

2 participants