Skip to main content

Data Types & Type Mapping

postgrejs decodes every PostgreSQL wire value into a native JS type through a DataType registry keyed by OID (object identifier). The default registry, GlobalTypeMap, covers every commonly used scalar and array type out of the box.

Built-in scalar types

Postgres typeJS typeNotes
boolboolean
byteaBuffer
charstringsingle-byte "char" type
bpcharstringblank-padded character(n)
namestring
textstring
xmlstring
varcharstring
uuidstringcanonical xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx form
boxobject{ x1, y1, x2, y2 }
circleobject{ x, y, r }
lsegobject{ x1, y1, x2, y2 }
pointobject{ x, y }
dateDate
timestring
timestampDate
timestamptzDate
float4number
float8number
int2number
int4number
numericnumberdecoded via parseFloat — see Fetching as String for exact precision
oidnumber
int8BigIntdowngraded to number automatically when the value fits in Number.MAX_SAFE_INTEGER
jsonstring | objectparsed with JSON.parse unless fetchAsString requests the raw text
jsonbstring | objectsame as json
int2vectornumber[]fixed-size vector type used internally by Postgres catalogs

Array types

Every scalar type above has an array counterpart registered under its own _-prefixed OID (e.g. int4_int4, timestamptz_timestamptz). Arrays are:

  • Decoded from the binary wire format (dimension count, bounds, and element OID are read directly from the array header).
  • Able to represent PostgreSQL's multidimensional arrays — a _int4 column holding {{1,2},{3,4}} decodes to [[1, 2], [3, 4]].
  • Detected automatically for parameters: passing a JS array as a query parameter picks the array OID whose elementsOID matches the first element's detected type.
const qr = await connection.query(
'select array[1, 2, 3]::int4[] as nums',
);
console.log(qr.rows[0][0]); // [1, 2, 3]

DataTypeMap and GlobalTypeMap

DataTypeMap is the registry class that maps an OID to a DataType descriptor. GlobalTypeMap is the default, process-wide instance every connection uses unless a call overrides it.

import { GlobalTypeMap, DataTypeOIDs } from 'postgrejs';

const boolType = GlobalTypeMap.get(DataTypeOIDs.bool);
MethodDescription
get(oid)Returns the DataType registered for an OID, or undefined.
register(dataTypes)Registers one or more DataType descriptors, keyed by their own oid. Registering an OID that already exists replaces it.
determine(value)Infers the best-matching OID for a plain JS value (used for parameters that are not wrapped in BindParam). Arrays are matched against each type's elementsOID.

See API: DataTypeMap for the full class reference.

Registering a custom type

To support a Postgres type postgrejs doesn't ship a mapping for (a custom domain, extension type, or one of the catalog types not registered by default), implement the DataType interface and register it — either into GlobalTypeMap for every connection, or into a private DataTypeMap used only for specific calls:

import { DataTypeMap, GlobalTypeMap, type DataType } from 'postgrejs';

const MyEnumType: DataType = {
name: 'my_enum',
oid: 123456, // the type's OID in your database
jsType: 'string',
isType: (v: any): boolean => typeof v === 'string',
decodeText: (v: string) => v,
encodeText: (v: any) => String(v),
};

// Global — every connection sees it
GlobalTypeMap.register(MyEnumType);

// Or scoped to just the calls that pass this map
const myTypeMap = new DataTypeMap(GlobalTypeMap);
myTypeMap.register(MyEnumType);

A DataType descriptor at minimum needs name, oid, jsType, isType, and a decodeText/decodeBinary pair matching the wire format(s) you support; add encodeText/encodeBinary to also send values of that type as query parameters. Set elementsOID to register the type as the array element of another OID.

Per-query type mapping

Both QueryOptions and StatementPrepareOptions accept a typeMap field that overrides GlobalTypeMap for a single query()/execute() call or a single prepared statement, without touching the global registry:

import { DataTypeMap, GlobalTypeMap } from 'postgrejs';

const customMap = new DataTypeMap(GlobalTypeMap);
customMap.register(MyEnumType);

const qr = await connection.query('select * from widgets', {
typeMap: customMap,
});

Text vs Binary Wire Format

PostgreSQL's wire protocol can transfer column values as text or binary. postgrejs defaults every column to binary (DataFormat.binary, per DEFAULT_COLUMN_FORMAT in the library's constants) since it avoids the cost of formatting/parsing decimal text for every row.

Override the format with columnFormat — a single DataFormat for every column, or an array assigning a format per column position:

import { DataFormat } from 'postgrejs';

// All columns as text
await connection.query('select id, name from customers', {
columnFormat: DataFormat.text,
});

// Per-column: first column binary, second column text
await connection.query('select id, name from customers', {
columnFormat: [DataFormat.binary, DataFormat.text],
});

Fetching as String

Some types lose precision or information when decoded to their native JS type — numeric decodes through parseFloat, which cannot represent values beyond number's precision, and date/time/timestamp/timestamptz collapse Postgres's infinity/-infinity into JS Infinity. Use fetchAsString (on DataMappingOptions, inherited by QueryOptions) to force specific OIDs to decode as their raw string representation instead:

import { DataTypeOIDs } from 'postgrejs';

const qr = await connection.query(
'select price from products where id = $1',
{
params: [1],
fetchAsString: [DataTypeOIDs.numeric],
},
);
console.log(qr.rows[0][0]); // "19.995000" — exact string, not a lossy float

See also