diff --git a/src/convert.ts b/src/convert.ts index be731ca..153120e 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -31,6 +31,12 @@ export function convertValue(raw: string, type: Converter): unknown { if (globalThis.Number.isNaN(n)) { throw new TypeMismatchError(`cannot convert ${JSON.stringify(raw)} to number`); } + // Integer strings that exceed safe integer range lose precision silently — reject them. + if (/^-?\d+$/.test(raw.trim()) && !globalThis.Number.isSafeInteger(n)) { + throw new TypeMismatchError( + `integer ${JSON.stringify(raw)} exceeds safe integer range; use BigInt`, + ); + } return n; } if (type === Boolean) { diff --git a/test/convert.test.ts b/test/convert.test.ts index d162444..5281d2a 100644 --- a/test/convert.test.ts +++ b/test/convert.test.ts @@ -38,6 +38,30 @@ describe("convertValue", () => { it("throws TypeMismatchError for empty string", () => { expect(() => convertValue("", Number)).toThrow(TypeMismatchError); }); + + it("accepts Number.MAX_SAFE_INTEGER", () => { + expect(convertValue(String(Number.MAX_SAFE_INTEGER), Number)).toBe(Number.MAX_SAFE_INTEGER); + }); + + it("accepts Number.MIN_SAFE_INTEGER", () => { + expect(convertValue(String(Number.MIN_SAFE_INTEGER), Number)).toBe(Number.MIN_SAFE_INTEGER); + }); + + it("throws TypeMismatchError for integer above MAX_SAFE_INTEGER", () => { + expect(() => convertValue(String(Number.MAX_SAFE_INTEGER + 1), Number)).toThrow( + TypeMismatchError, + ); + }); + + it("throws TypeMismatchError for integer below MIN_SAFE_INTEGER", () => { + expect(() => convertValue(String(Number.MIN_SAFE_INTEGER - 1), Number)).toThrow( + TypeMismatchError, + ); + }); + + it("does not throw for large float (non-integer) above MAX_SAFE_INTEGER", () => { + expect(convertValue("1e20", Number)).toBe(1e20); + }); }); describe("Boolean converter", () => {