Chelabase
TypeScript client

Big numbers

Reading int8 and numeric values exactly, and why a typed client requires types.

Postgres writes int8 and numeric as JSON numbers with every digit. JavaScript's number holds integers exactly only up to ±(2^53 − 1), so the client reads those values from their source text. It finds them with the generated types map (pass it to createClient), including through select aliases, returning keys, a function's scalar return and a set-returning function's columns.

A typed client requires types

createClient<Database>({ url, key }) without types is a compile error, because without the map the client can't tell an int8 or a numeric from any other number (a numeric typed string would come back as a number).

import {  } from "@chelabase/client";
import { type  } from "./db";

const  = <>({ url: "http://api.chelabase.localhost:8000", key: "chk_pub_…" });
No overload matches this call. The last overload gave the following error. Argument of type '{ url: string; key: string; }' is not assignable to parameter of type 'ClientOptions & { bigint?: false | undefined; } & { types: TypeMap; }'. Type '{ url: string; key: string; }' is not assignable to type '{ url: string; key: string; fetch?: ((input: URL | RequestInfo, init?: RequestInit | undefined) => Promise<Response>) | undefined; types?: TypeMap | undefined; } & { ...; } & { ...; } & { ...; }'. Property 'accessToken' is missing in type '{ url: string; key: string; }' but required in type '{ accessToken: () => string | Promise<string | undefined> | undefined; auth?: undefined; }'.

The check is compile-time only, since the generic is erased at run time. bigint: true also throws at run time when types is missing. The untyped createClient({ url, key }) needs no types.

What you get

PostgresDefaultbigint: true
int2, int4numbernumber
int8number; throws UnsafeIntegerError outside the safe rangebigint, always
numericstring with the exact digitsstring

Use bigint: true together with types to get bigint for all int8 columns, arguments and returns. count() is the exception: it always resolves to a number, and a count beyond 2^53 throws UnsafeIntegerError.

const  = <>({ : "http://api.chelabase.localhost:8000", : "chk_pub_…", , : true });

const row = await .("orders").("id", "total").(1);
const row: {
    id: bigint;
    total: string;
}[]
const next = await .("next_id");
const next: bigint
  • A column's type never depends on its value: an int8 is number or bigint for the whole client, chosen by bigint and followed by the types.
  • Forgetting bigint: true is loud, not silent: an int8 outside the safe range throws UnsafeIntegerError (with column and the exact source digits) instead of returning a rounded number.
  • Without types, any integer literal outside the safe range throws UnsafeIntegerError, wherever it is; other numbers stay numbers.
  • With types, a value the map doesn't cover (a column added after you got the types, a function with no int8 or numeric return) still throws UnsafeIntegerError when it's a top-level integer, or an array of them, outside the safe range. It's never rounded silently; download or fetch the types again.
  • Only top-level row keys (and scalar returns) are mapped. Numbers inside json and jsonb objects are parsed as plain JSON numbers.
  • Writes are exact too: a bigint in insert, update, where or call arguments is sent as its digits. Send numeric values as strings.

Runtime requirement

Reading exactly uses JSON.parse source text access, available in Node 21+, Chrome 114+, Firefox 135+ and Safari 18.4+.

On an older runtime, createClient throws when given bigint: true, or types that include a numeric column. The default mode still works and still throws UnsafeIntegerError for an int8 outside the safe range (checked after a plain parse). There, an integer is recognised by its printed form (no exponent or point), so a float between 2^53 and 1e21 (1.5e20) is also refused; floats printed with an exponent (1e300) are not.

On this page