From ec4c898dc3cf685a5545f50caf48cc86da2ef42a Mon Sep 17 00:00:00 2001 From: WolfieLeader Date: Thu, 19 Mar 2026 03:47:02 +0200 Subject: [PATCH 1/6] feat(type): add number.finite keyword to reject Infinity and NaN (#1598) --- ark/type/__tests__/keywords/number.test.ts | 16 ++++++++++++++++ ark/type/keywords/number.ts | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ark/type/__tests__/keywords/number.test.ts b/ark/type/__tests__/keywords/number.test.ts index 6de9e3645f..5f89972257 100644 --- a/ark/type/__tests__/keywords/number.test.ts +++ b/ark/type/__tests__/keywords/number.test.ts @@ -54,6 +54,22 @@ contextualize(() => { attest(Safe(NaN).toString()).snap("must be a number (was NaN)") }) + it("finite", () => { + const Finite = type("number.finite") + attest(Finite(42)).snap(42) + attest(Finite(-3.14)).snap(-3.14) + attest(Finite(0)).snap(0) + attest(Finite(Infinity).toString()).snap( + "must be a finite number (was Infinity)" + ) + attest(Finite(-Infinity).toString()).snap( + "must be a finite number (was -Infinity)" + ) + attest(Finite(NaN).toString()).snap( + "must be a number (was NaN)" + ) + }) + it("doesn't allow NaN by default", () => { attest(type.number.allows(Number.NaN)).equals(false) attest(type.number(Number.NaN).toString()).snap( diff --git a/ark/type/keywords/number.ts b/ark/type/keywords/number.ts index ba7fe57104..567e63c0c3 100644 --- a/ark/type/keywords/number.ts +++ b/ark/type/keywords/number.ts @@ -34,11 +34,23 @@ export const integer = rootSchema({ divisor: 1 }) +const finite = rootSchema({ + domain: { + domain: "number", + numberAllowsNaN: false + }, + predicate: { + meta: "a finite number", + predicate: (n: number) => Number.isFinite(n) + } +}) + export const number: number.module = Scope.module( { root: intrinsic.number, integer, epoch, + finite, safe: rootSchema({ domain: { domain: "number", @@ -64,6 +76,7 @@ export declare namespace number { export type $ = { root: number epoch: number + finite: number integer: number safe: number NaN: number From 78f9c9dcb54e406b1072d6940eea8cdb94b67e2d Mon Sep 17 00:00:00 2001 From: WolfieLeader Date: Thu, 19 Mar 2026 04:05:57 +0200 Subject: [PATCH 2/6] style: fix prettier formatting in number test --- ark/type/__tests__/keywords/number.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ark/type/__tests__/keywords/number.test.ts b/ark/type/__tests__/keywords/number.test.ts index 5f89972257..c871412fe1 100644 --- a/ark/type/__tests__/keywords/number.test.ts +++ b/ark/type/__tests__/keywords/number.test.ts @@ -65,9 +65,7 @@ contextualize(() => { attest(Finite(-Infinity).toString()).snap( "must be a finite number (was -Infinity)" ) - attest(Finite(NaN).toString()).snap( - "must be a number (was NaN)" - ) + attest(Finite(NaN).toString()).snap("must be a number (was NaN)") }) it("doesn't allow NaN by default", () => { From 4f4ec015d418b7155f7f6d6872644403418fdb40 Mon Sep 17 00:00:00 2001 From: WolfieLeader Date: Thu, 19 Mar 2026 04:13:35 +0200 Subject: [PATCH 3/6] refactor: use min/max bounds instead of predicate for number.finite Matches the pattern used by number.safe. min/max bounds are composable, JIT-compiled, and introspectable by the fast-check arbitrary generator. --- ark/type/__tests__/keywords/number.test.ts | 2 ++ ark/type/keywords/number.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ark/type/__tests__/keywords/number.test.ts b/ark/type/__tests__/keywords/number.test.ts index c871412fe1..b01cfb779f 100644 --- a/ark/type/__tests__/keywords/number.test.ts +++ b/ark/type/__tests__/keywords/number.test.ts @@ -59,6 +59,8 @@ contextualize(() => { attest(Finite(42)).snap(42) attest(Finite(-3.14)).snap(-3.14) attest(Finite(0)).snap(0) + attest(Finite.allows(Number.MAX_VALUE)).equals(true) + attest(Finite.allows(-Number.MAX_VALUE)).equals(true) attest(Finite(Infinity).toString()).snap( "must be a finite number (was Infinity)" ) diff --git a/ark/type/keywords/number.ts b/ark/type/keywords/number.ts index 567e63c0c3..fc3529117e 100644 --- a/ark/type/keywords/number.ts +++ b/ark/type/keywords/number.ts @@ -39,9 +39,13 @@ const finite = rootSchema({ domain: "number", numberAllowsNaN: false }, - predicate: { - meta: "a finite number", - predicate: (n: number) => Number.isFinite(n) + min: { + rule: -Number.MAX_VALUE, + meta: "a finite number" + }, + max: { + rule: Number.MAX_VALUE, + meta: "a finite number" } }) From 372718ecf0fd3b553067aea87b9a5cf16f769666 Mon Sep 17 00:00:00 2001 From: WolfieLeader Date: Thu, 19 Mar 2026 05:04:05 +0200 Subject: [PATCH 4/6] test: register number.finite in allConfig integration test --- ark/type/__tests__/integration/allConfig.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ark/type/__tests__/integration/allConfig.ts b/ark/type/__tests__/integration/allConfig.ts index 2208e49f04..ce85d43541 100644 --- a/ark/type/__tests__/integration/allConfig.ts +++ b/ark/type/__tests__/integration/allConfig.ts @@ -86,6 +86,9 @@ configure({ "number.safe": { description: "configured" }, + "number.finite": { + description: "configured" + }, "number.NaN": { description: "configured" }, From 52c277f95600c146e80f93254c7d36a7f74a5616 Mon Sep 17 00:00:00 2001 From: WolfieLeader Date: Thu, 19 Mar 2026 05:12:25 +0200 Subject: [PATCH 5/6] chore: regenerate allConfig.ts to include number.finite --- ark/type/__tests__/integration/allConfig.ts | 508 ++++++++++---------- 1 file changed, 254 insertions(+), 254 deletions(-) diff --git a/ark/type/__tests__/integration/allConfig.ts b/ark/type/__tests__/integration/allConfig.ts index ce85d43541..f65b79059b 100644 --- a/ark/type/__tests__/integration/allConfig.ts +++ b/ark/type/__tests__/integration/allConfig.ts @@ -1,258 +1,258 @@ import { configure } from "arktype/config" configure({ - keywords: { - bigint: { - description: "configured" - }, - boolean: { - description: "configured" - }, - false: { - description: "configured" - }, - never: { - description: "configured" - }, - null: { - description: "configured" - }, - symbol: { - description: "configured" - }, - true: { - description: "configured" - }, - undefined: { - description: "configured" - }, - Date: { - description: "configured" - }, - Error: { - description: "configured" - }, - Function: { - description: "configured" - }, - Map: { - description: "configured" - }, - Promise: { - description: "configured" - }, - RegExp: { - description: "configured" - }, - Set: { - description: "configured" - }, - WeakMap: { - description: "configured" - }, - WeakSet: { - description: "configured" - }, - ArrayBuffer: { - description: "configured" - }, - Blob: { - description: "configured" - }, - File: { - description: "configured" - }, - Headers: { - description: "configured" - }, - Request: { - description: "configured" - }, - Response: { - description: "configured" - }, - URL: { - description: "configured" - }, - Key: { - description: "configured" - }, - "number.integer": { - description: "configured" - }, - "number.epoch": { - description: "configured" - }, - "number.safe": { - description: "configured" - }, - "number.finite": { - description: "configured" - }, - "number.NaN": { - description: "configured" - }, - "number.Infinity": { - description: "configured" - }, - "number.NegativeInfinity": { - description: "configured" - }, - "object.json.stringify": { - description: "configured" - }, - "string.alpha": { - description: "configured" - }, - "string.alphanumeric": { - description: "configured" - }, - "string.hex": { - description: "configured" - }, - "string.base64.url": { - description: "configured" - }, - "string.capitalize.preformatted": { - description: "configured" - }, - "string.creditCard": { - description: "configured" - }, - "string.date.parse": { - description: "configured" - }, - "string.date.iso.parse": { - description: "configured" - }, - "string.date.epoch.parse": { - description: "configured" - }, - "string.digits": { - description: "configured" - }, - "string.email": { - description: "configured" - }, - "string.integer.parse": { - description: "configured" - }, - "string.ip.v4": { - description: "configured" - }, - "string.ip.v6": { - description: "configured" - }, - "string.json.parse": { - description: "configured" - }, - "string.lower.preformatted": { - description: "configured" - }, - "string.normalize.NFC.preformatted": { - description: "configured" - }, - "string.normalize.NFD.preformatted": { - description: "configured" - }, - "string.normalize.NFKC.preformatted": { - description: "configured" - }, - "string.normalize.NFKD.preformatted": { - description: "configured" - }, - "string.numeric.parse": { - description: "configured" - }, - "string.regex": { - description: "configured" - }, - "string.semver": { - description: "configured" - }, - "string.trim.preformatted": { - description: "configured" - }, - "string.upper.preformatted": { - description: "configured" - }, - "string.url.parse": { - description: "configured" - }, - "string.uuid.v1": { - description: "configured" - }, - "string.uuid.v2": { - description: "configured" - }, - "string.uuid.v3": { - description: "configured" - }, - "string.uuid.v4": { - description: "configured" - }, - "string.uuid.v5": { - description: "configured" - }, - "string.uuid.v6": { - description: "configured" - }, - "string.uuid.v7": { - description: "configured" - }, - "string.uuid.v8": { - description: "configured" - }, - "unknown.any": { - description: "configured" - }, - "Array.readonly": { - description: "configured" - }, - "Array.index": { - description: "configured" - }, - "FormData.value": { - description: "configured" - }, - "FormData.parsed": { - description: "configured" - }, - "FormData.parse": { - description: "configured" - }, - "TypedArray.Int8": { - description: "configured" - }, - "TypedArray.Uint8": { - description: "configured" - }, - "TypedArray.Uint8Clamped": { - description: "configured" - }, - "TypedArray.Int16": { - description: "configured" - }, - "TypedArray.Uint16": { - description: "configured" - }, - "TypedArray.Int32": { - description: "configured" - }, - "TypedArray.Uint32": { - description: "configured" - }, - "TypedArray.Float32": { - description: "configured" - }, - "TypedArray.Float64": { - description: "configured" - }, - "TypedArray.BigInt64": { - description: "configured" - }, - "TypedArray.BigUint64": { - description: "configured" - } - } + "keywords": { + "bigint": { + "description": "configured" + }, + "boolean": { + "description": "configured" + }, + "false": { + "description": "configured" + }, + "never": { + "description": "configured" + }, + "null": { + "description": "configured" + }, + "symbol": { + "description": "configured" + }, + "true": { + "description": "configured" + }, + "undefined": { + "description": "configured" + }, + "Date": { + "description": "configured" + }, + "Error": { + "description": "configured" + }, + "Function": { + "description": "configured" + }, + "Map": { + "description": "configured" + }, + "Promise": { + "description": "configured" + }, + "RegExp": { + "description": "configured" + }, + "Set": { + "description": "configured" + }, + "WeakMap": { + "description": "configured" + }, + "WeakSet": { + "description": "configured" + }, + "ArrayBuffer": { + "description": "configured" + }, + "Blob": { + "description": "configured" + }, + "File": { + "description": "configured" + }, + "Headers": { + "description": "configured" + }, + "Request": { + "description": "configured" + }, + "Response": { + "description": "configured" + }, + "URL": { + "description": "configured" + }, + "Key": { + "description": "configured" + }, + "number.integer": { + "description": "configured" + }, + "number.epoch": { + "description": "configured" + }, + "number.finite": { + "description": "configured" + }, + "number.safe": { + "description": "configured" + }, + "number.NaN": { + "description": "configured" + }, + "number.Infinity": { + "description": "configured" + }, + "number.NegativeInfinity": { + "description": "configured" + }, + "object.json.stringify": { + "description": "configured" + }, + "string.alpha": { + "description": "configured" + }, + "string.alphanumeric": { + "description": "configured" + }, + "string.hex": { + "description": "configured" + }, + "string.base64.url": { + "description": "configured" + }, + "string.capitalize.preformatted": { + "description": "configured" + }, + "string.creditCard": { + "description": "configured" + }, + "string.date.parse": { + "description": "configured" + }, + "string.date.iso.parse": { + "description": "configured" + }, + "string.date.epoch.parse": { + "description": "configured" + }, + "string.digits": { + "description": "configured" + }, + "string.email": { + "description": "configured" + }, + "string.integer.parse": { + "description": "configured" + }, + "string.ip.v4": { + "description": "configured" + }, + "string.ip.v6": { + "description": "configured" + }, + "string.json.parse": { + "description": "configured" + }, + "string.lower.preformatted": { + "description": "configured" + }, + "string.normalize.NFC.preformatted": { + "description": "configured" + }, + "string.normalize.NFD.preformatted": { + "description": "configured" + }, + "string.normalize.NFKC.preformatted": { + "description": "configured" + }, + "string.normalize.NFKD.preformatted": { + "description": "configured" + }, + "string.numeric.parse": { + "description": "configured" + }, + "string.regex": { + "description": "configured" + }, + "string.semver": { + "description": "configured" + }, + "string.trim.preformatted": { + "description": "configured" + }, + "string.upper.preformatted": { + "description": "configured" + }, + "string.url.parse": { + "description": "configured" + }, + "string.uuid.v1": { + "description": "configured" + }, + "string.uuid.v2": { + "description": "configured" + }, + "string.uuid.v3": { + "description": "configured" + }, + "string.uuid.v4": { + "description": "configured" + }, + "string.uuid.v5": { + "description": "configured" + }, + "string.uuid.v6": { + "description": "configured" + }, + "string.uuid.v7": { + "description": "configured" + }, + "string.uuid.v8": { + "description": "configured" + }, + "unknown.any": { + "description": "configured" + }, + "Array.readonly": { + "description": "configured" + }, + "Array.index": { + "description": "configured" + }, + "FormData.value": { + "description": "configured" + }, + "FormData.parsed": { + "description": "configured" + }, + "FormData.parse": { + "description": "configured" + }, + "TypedArray.Int8": { + "description": "configured" + }, + "TypedArray.Uint8": { + "description": "configured" + }, + "TypedArray.Uint8Clamped": { + "description": "configured" + }, + "TypedArray.Int16": { + "description": "configured" + }, + "TypedArray.Uint16": { + "description": "configured" + }, + "TypedArray.Int32": { + "description": "configured" + }, + "TypedArray.Uint32": { + "description": "configured" + }, + "TypedArray.Float32": { + "description": "configured" + }, + "TypedArray.Float64": { + "description": "configured" + }, + "TypedArray.BigInt64": { + "description": "configured" + }, + "TypedArray.BigUint64": { + "description": "configured" + } + } }) From 7975652349229be21fd7f80e90592acf98f790bf Mon Sep 17 00:00:00 2001 From: WolfieLeader Date: Thu, 19 Mar 2026 05:15:43 +0200 Subject: [PATCH 6/6] style: format allConfig.ts with prettier The generator uses JSON.stringify with spaces but the project enforces tabs via prettier. --- ark/type/__tests__/integration/allConfig.ts | 508 ++++++++++---------- 1 file changed, 254 insertions(+), 254 deletions(-) diff --git a/ark/type/__tests__/integration/allConfig.ts b/ark/type/__tests__/integration/allConfig.ts index f65b79059b..86f70be4d5 100644 --- a/ark/type/__tests__/integration/allConfig.ts +++ b/ark/type/__tests__/integration/allConfig.ts @@ -1,258 +1,258 @@ import { configure } from "arktype/config" configure({ - "keywords": { - "bigint": { - "description": "configured" - }, - "boolean": { - "description": "configured" - }, - "false": { - "description": "configured" - }, - "never": { - "description": "configured" - }, - "null": { - "description": "configured" - }, - "symbol": { - "description": "configured" - }, - "true": { - "description": "configured" - }, - "undefined": { - "description": "configured" - }, - "Date": { - "description": "configured" - }, - "Error": { - "description": "configured" - }, - "Function": { - "description": "configured" - }, - "Map": { - "description": "configured" - }, - "Promise": { - "description": "configured" - }, - "RegExp": { - "description": "configured" - }, - "Set": { - "description": "configured" - }, - "WeakMap": { - "description": "configured" - }, - "WeakSet": { - "description": "configured" - }, - "ArrayBuffer": { - "description": "configured" - }, - "Blob": { - "description": "configured" - }, - "File": { - "description": "configured" - }, - "Headers": { - "description": "configured" - }, - "Request": { - "description": "configured" - }, - "Response": { - "description": "configured" - }, - "URL": { - "description": "configured" - }, - "Key": { - "description": "configured" - }, - "number.integer": { - "description": "configured" - }, - "number.epoch": { - "description": "configured" - }, - "number.finite": { - "description": "configured" - }, - "number.safe": { - "description": "configured" - }, - "number.NaN": { - "description": "configured" - }, - "number.Infinity": { - "description": "configured" - }, - "number.NegativeInfinity": { - "description": "configured" - }, - "object.json.stringify": { - "description": "configured" - }, - "string.alpha": { - "description": "configured" - }, - "string.alphanumeric": { - "description": "configured" - }, - "string.hex": { - "description": "configured" - }, - "string.base64.url": { - "description": "configured" - }, - "string.capitalize.preformatted": { - "description": "configured" - }, - "string.creditCard": { - "description": "configured" - }, - "string.date.parse": { - "description": "configured" - }, - "string.date.iso.parse": { - "description": "configured" - }, - "string.date.epoch.parse": { - "description": "configured" - }, - "string.digits": { - "description": "configured" - }, - "string.email": { - "description": "configured" - }, - "string.integer.parse": { - "description": "configured" - }, - "string.ip.v4": { - "description": "configured" - }, - "string.ip.v6": { - "description": "configured" - }, - "string.json.parse": { - "description": "configured" - }, - "string.lower.preformatted": { - "description": "configured" - }, - "string.normalize.NFC.preformatted": { - "description": "configured" - }, - "string.normalize.NFD.preformatted": { - "description": "configured" - }, - "string.normalize.NFKC.preformatted": { - "description": "configured" - }, - "string.normalize.NFKD.preformatted": { - "description": "configured" - }, - "string.numeric.parse": { - "description": "configured" - }, - "string.regex": { - "description": "configured" - }, - "string.semver": { - "description": "configured" - }, - "string.trim.preformatted": { - "description": "configured" - }, - "string.upper.preformatted": { - "description": "configured" - }, - "string.url.parse": { - "description": "configured" - }, - "string.uuid.v1": { - "description": "configured" - }, - "string.uuid.v2": { - "description": "configured" - }, - "string.uuid.v3": { - "description": "configured" - }, - "string.uuid.v4": { - "description": "configured" - }, - "string.uuid.v5": { - "description": "configured" - }, - "string.uuid.v6": { - "description": "configured" - }, - "string.uuid.v7": { - "description": "configured" - }, - "string.uuid.v8": { - "description": "configured" - }, - "unknown.any": { - "description": "configured" - }, - "Array.readonly": { - "description": "configured" - }, - "Array.index": { - "description": "configured" - }, - "FormData.value": { - "description": "configured" - }, - "FormData.parsed": { - "description": "configured" - }, - "FormData.parse": { - "description": "configured" - }, - "TypedArray.Int8": { - "description": "configured" - }, - "TypedArray.Uint8": { - "description": "configured" - }, - "TypedArray.Uint8Clamped": { - "description": "configured" - }, - "TypedArray.Int16": { - "description": "configured" - }, - "TypedArray.Uint16": { - "description": "configured" - }, - "TypedArray.Int32": { - "description": "configured" - }, - "TypedArray.Uint32": { - "description": "configured" - }, - "TypedArray.Float32": { - "description": "configured" - }, - "TypedArray.Float64": { - "description": "configured" - }, - "TypedArray.BigInt64": { - "description": "configured" - }, - "TypedArray.BigUint64": { - "description": "configured" - } - } + keywords: { + bigint: { + description: "configured" + }, + boolean: { + description: "configured" + }, + false: { + description: "configured" + }, + never: { + description: "configured" + }, + null: { + description: "configured" + }, + symbol: { + description: "configured" + }, + true: { + description: "configured" + }, + undefined: { + description: "configured" + }, + Date: { + description: "configured" + }, + Error: { + description: "configured" + }, + Function: { + description: "configured" + }, + Map: { + description: "configured" + }, + Promise: { + description: "configured" + }, + RegExp: { + description: "configured" + }, + Set: { + description: "configured" + }, + WeakMap: { + description: "configured" + }, + WeakSet: { + description: "configured" + }, + ArrayBuffer: { + description: "configured" + }, + Blob: { + description: "configured" + }, + File: { + description: "configured" + }, + Headers: { + description: "configured" + }, + Request: { + description: "configured" + }, + Response: { + description: "configured" + }, + URL: { + description: "configured" + }, + Key: { + description: "configured" + }, + "number.integer": { + description: "configured" + }, + "number.epoch": { + description: "configured" + }, + "number.finite": { + description: "configured" + }, + "number.safe": { + description: "configured" + }, + "number.NaN": { + description: "configured" + }, + "number.Infinity": { + description: "configured" + }, + "number.NegativeInfinity": { + description: "configured" + }, + "object.json.stringify": { + description: "configured" + }, + "string.alpha": { + description: "configured" + }, + "string.alphanumeric": { + description: "configured" + }, + "string.hex": { + description: "configured" + }, + "string.base64.url": { + description: "configured" + }, + "string.capitalize.preformatted": { + description: "configured" + }, + "string.creditCard": { + description: "configured" + }, + "string.date.parse": { + description: "configured" + }, + "string.date.iso.parse": { + description: "configured" + }, + "string.date.epoch.parse": { + description: "configured" + }, + "string.digits": { + description: "configured" + }, + "string.email": { + description: "configured" + }, + "string.integer.parse": { + description: "configured" + }, + "string.ip.v4": { + description: "configured" + }, + "string.ip.v6": { + description: "configured" + }, + "string.json.parse": { + description: "configured" + }, + "string.lower.preformatted": { + description: "configured" + }, + "string.normalize.NFC.preformatted": { + description: "configured" + }, + "string.normalize.NFD.preformatted": { + description: "configured" + }, + "string.normalize.NFKC.preformatted": { + description: "configured" + }, + "string.normalize.NFKD.preformatted": { + description: "configured" + }, + "string.numeric.parse": { + description: "configured" + }, + "string.regex": { + description: "configured" + }, + "string.semver": { + description: "configured" + }, + "string.trim.preformatted": { + description: "configured" + }, + "string.upper.preformatted": { + description: "configured" + }, + "string.url.parse": { + description: "configured" + }, + "string.uuid.v1": { + description: "configured" + }, + "string.uuid.v2": { + description: "configured" + }, + "string.uuid.v3": { + description: "configured" + }, + "string.uuid.v4": { + description: "configured" + }, + "string.uuid.v5": { + description: "configured" + }, + "string.uuid.v6": { + description: "configured" + }, + "string.uuid.v7": { + description: "configured" + }, + "string.uuid.v8": { + description: "configured" + }, + "unknown.any": { + description: "configured" + }, + "Array.readonly": { + description: "configured" + }, + "Array.index": { + description: "configured" + }, + "FormData.value": { + description: "configured" + }, + "FormData.parsed": { + description: "configured" + }, + "FormData.parse": { + description: "configured" + }, + "TypedArray.Int8": { + description: "configured" + }, + "TypedArray.Uint8": { + description: "configured" + }, + "TypedArray.Uint8Clamped": { + description: "configured" + }, + "TypedArray.Int16": { + description: "configured" + }, + "TypedArray.Uint16": { + description: "configured" + }, + "TypedArray.Int32": { + description: "configured" + }, + "TypedArray.Uint32": { + description: "configured" + }, + "TypedArray.Float32": { + description: "configured" + }, + "TypedArray.Float64": { + description: "configured" + }, + "TypedArray.BigInt64": { + description: "configured" + }, + "TypedArray.BigUint64": { + description: "configured" + } + } })