DataTypes is the vocabulary you use to declare a column. Each type is a smallgetDataTypeSql() on the dialect turns a DataTypes.BOOLEAN() intoBOOLEAN on PostgreSQL, TINYINT(1) on MySQL, BIT on SQL Server andINTEGER on SQLite. Writing DataTypes.BOOLEAN() once therefore gives you aimport { Prorm, DataTypes } from 'ts-prorm-orm';
const prorm = new Prorm({ dialect: 'postgres', database: 'shop' });
const Product = prorm.define('Product', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
sku: { type: DataTypes.STRING(64), allowNull: false, unique: true },
price: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
inStock: { type: DataTypes.BOOLEAN(), defaultValue: true },
tags: { type: DataTypes.ARRAY(DataTypes.STRING(50)) },
attributes:{ type: DataTypes.JSONB() },
releasedAt:{ type: DataTypes.DATE() },
});[!WARNING]
On MySQL thetagsarray became aVARCHAR(255). AnARRAYof fifty tags
will not fit, and nothing in the database knows it is a list. If the model
must run on more than PostgreSQL, declare lists asJSON()instead.
DataTypes.STRING(100)STRING(100) are the same thing:import { STRING, INTEGER, JSONB } from 'ts-prorm-orm';getDataTypeSql(). Split by family rather than given as one wide grid, becausetypeMapping: 'legacy' keeps 4.8's DDL for existing'modern' and which columns need a manual migration.| DataType | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
STRING(100) | VARCHAR(100) | VARCHAR(100) | VARCHAR(100) | NVARCHAR(100) | VARCHAR2(100) |
STRING() | VARCHAR(255) | VARCHAR(255) | VARCHAR(255) | NVARCHAR(255) | VARCHAR2(255) |
CHAR(2) | CHAR(2) | CHAR(2) | CHAR(2) | CHAR(2) | CHAR(2) |
TEXT() | TEXT | TEXT | TEXT | NVARCHAR(MAX) | CLOB |
| DataType | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
INTEGER() | INTEGER | INTEGER | INT | INT | NUMBER(10) |
BIGINT() | BIGINT | BIGINT | BIGINT | BIGINT | NUMBER(19) |
FLOAT() | FLOAT(10,2) | REAL | FLOAT | FLOAT | FLOAT |
DOUBLE() | DOUBLE(10,2) | DOUBLE PRECISION | DOUBLE | FLOAT | FLOAT |
DECIMAL(10,2) | DECIMAL(10,2) | DECIMAL(10,2) | DECIMAL(10,2) | DECIMAL(10,2) | NUMBER(10,2) |
DECIMAL is exact. FLOAT and DOUBLE are binary floating point on| DataType | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
BOOLEAN() | INTEGER | BOOLEAN | TINYINT(1) | BIT | NUMBER(1) |
DATE() | DATETIME | TIMESTAMP | DATETIME | DATETIME2 | DATE |
DATEONLY() | DATE | DATE | DATE | DATE | DATE |
TIME() | TIME | TIME | TIME | TIME | VARCHAR2(8) |
DATE carries a time component, so DATEONLY() and DATE() are the| DataType | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
JSON() | TEXT | JSON | JSON | NVARCHAR(MAX) | CLOB |
JSONB() | TEXT | JSONB | JSON | NVARCHAR(MAX) | BLOB |
BLOB() | BLOB | BYTEA | BLOB | VARBINARY(MAX) | BLOB |
UUID() | TEXT | UUID | CHAR(36) | UNIQUEIDENTIFIER | RAW(16) |
ARRAY(INTEGER()) | TEXT | INTEGER[] | VARCHAR(255) | NVARCHAR(255) | VARCHAR2(255) |
TEXT, values still round-trip as parsed JavaScript| DataType | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
ENUM('a','b') | TEXT CHECK(col IN ('a','b')) | VARCHAR(255) CHECK (col IN ('a', 'b')) † | ENUM('a','b') | NVARCHAR(255) | VARCHAR2(255) |
GEOMETRY() | BLOB | geometry | GEOMETRY | GEOMETRY | SDO_GEOMETRY |
VIRTUAL() | (no column) | (no column) | (no column) | (no column) | VARCHAR2(255) |
ENUM(...) column syntax, so under the default'legacy' mapping ENUM() is a string column with a CHECK on the value setVARCHAR(255) that accepted anyINTEGER()/BIGINT() with autoIncrement: true become SERIAL/BIGSERIALINTEGER PRIMARY KEY AUTOINCREMENT on SQLite.ARRAY only exists natively on PostgreSQL. On every other dialect the column| Type | Notes |
|---|---|
STRING(length?, binary?) | VARCHAR(length). With no length it renders TEXT. binary: true adds a BINARY collation. |
VARCHAR(length = 255, binary?) | Always VARCHAR, never degrades to TEXT. |
CHAR(length = 1) | Fixed width. |
TEXT('tiny' | 'medium' | 'long') | Unbounded text; the size hint maps to TINYTEXT/MEDIUMTEXT/LONGTEXT on MySQL. |
TINYTEXT(), MEDIUMTEXT(), LONGTEXT() | Explicit MySQL text sizes; portable fallbacks elsewhere. |
| Type | Notes |
|---|---|
INTEGER(), TINYINT(), SMALLINT(), MEDIUMINT(), BIGINT() | Integer family. All support .UNSIGNED() and .ZEROFILL() chaining on MySQL/MariaDB. |
FLOAT(length?, decimals?), DOUBLE(...), REAL(...) | Approximate numerics. |
DECIMAL(precision, scale) | Exact numerics — use this for money. |
{ views: { type: DataTypes.INTEGER().UNSIGNED() } } // INT UNSIGNED on MySQLBIGINT values are handed back as JavaScript BigInt when the driver returnsNumber, which would lose[!CAUTION]
Whether the driver returns aBigIntis the driver's choice. With the default
SQLite driver settings, writing9007199254740993nand reading it back gives
the number9007199254740992— off by one, silently. Keep identifiers that can
exceed 2^53 (snowflake IDs, external account numbers) in aSTRINGcolumn, or
enable your driver's big-number mode.
BOOLEAN() — read back as a real true/false regardless of whether the1, '1' or true.| Type | Notes |
|---|---|
DATE(precision?, timezone?) | Date and time. timezone: true gives a zoned type under typeMapping: 'modern' (see below). |
DATEONLY() | Calendar date, no time. |
TIME(precision?, timezone?) | Time of day. |
DATETIME(), TIMESTAMP() | Dialect-specific spellings when you want them explicitly. |
NOW | Not a column type — a default value meaning "the database's current timestamp". |
Date instances even on SQLite, which stores them as ISODataTypes.DATE(3, true) renders:typeMapping | SQLite | PostgreSQL | MySQL | SQL Server | Oracle |
|---|---|---|---|---|---|
'legacy' (default) | DATETIME | TIMESTAMPTZ(3) | DATETIME(3) | DATETIME2(3) | DATE |
'modern' | DATETIME | TIMESTAMPTZ(3) | TIMESTAMP(3) | DATETIMEOFFSET(3) | TIMESTAMP(3) WITH TIME ZONE |
[!IMPORTANT]
Under the default'legacy'mapping, PostgreSQL (and the rest of the
PostgreSQL family) keeps the zone:DATE(3, true)isTIMESTAMPTZ(3).
Before 4.9.2 the legacy mapping dropped it and createdTIMESTAMP(3), so a
column created by an older release has no zone until you migrate it. MySQL, SQL Server
and Oracle still drop the zone under'legacy'; opt in withnew Prorm({ …, typeMapping: 'modern' })for new databases.
const attributes = { createdAt: { type: DataTypes.DATE(), defaultValue: DataTypes.NOW } };DataTypes.NOW, DataTypes.NOW() and fn('NOW') all become the engine'sDEFAULT CURRENT_TIMESTAMP on most engines,now() on ClickHouse and CURRENT_TIMESTAMP() on BigQuery.[!NOTE]
Older releases wrote the uncalledDataTypes.NOWas the textDEFAULT '() => new NOW()'on the PostgreSQL and MySQL families, HANA, Db2,
BigQuery, Snowflake, ClickHouse and DuckDB, andcreateTable()failed on
Firebird, Teradata, Vertica and several others. A table created that way
still has the text default; replace it withchangeColumnor a migration.
| Type | Notes |
|---|---|
UUID() | Native UUID/UNIQUEIDENTIFIER where available, CHAR(36)/TEXT elsewhere. |
UUIDV1, UUIDV4 | Default-value generators, not column types. Use them as a defaultValue (see below). |
DataTypes.UUIDV4, DataTypes.UUIDV1 or any() => randomUUID() — is evaluated in JavaScript bycreate() and written in the INSERT. It is not a database default, so theCREATE TABLE carries no DEFAULT for it:const Order = prorm.define(
'Order',
{
publicId: { type: DataTypes.UUID(), allowNull: false, unique: true, defaultValue: DataTypes.UUIDV4 },
total: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
},
{ tableName: 'orders', timestamps: false },
);
await Order.create({ total: 12.5 });allowNull: false on such a column. DataTypes.NOW is different: it stays aCURRENT_TIMESTAMP).[!NOTE]
Before 4.9.2,define()models did not send a generator default in theINSERT, andCREATE TABLEcarried the generator's source text as the
column default (DEFAULT '() => new UUIDV4()'), which is what those rows
stored. Tables created by an older release may still have that default;
drop it withchangeColumnor a migration.
| Type | Notes |
|---|---|
JSON() | Objects and arrays are serialised on write and parsed back on read. |
JSONB() | PostgreSQL binary JSON — indexable, supports containment operators. Falls back to JSON/TEXT elsewhere. |
HSTORE() | PostgreSQL key/value column. |
await Product.create({ attributes: { colour: 'red', sizes: [1, 2, 3] } });const p = await Product.findByPk(1);
p.attributes.colour; // 'red' — already parsedJSON/JSONB, MySQL JSON)TEXT/NVARCHAR(MAX)/CLOB —Behaviour change. Models built with prorm.define()used to hand back the
raw JSON string on text-backed dialects. If you have code doing its ownJSON.parse()on SQLite, drop it — it would now be parsing an object.
PostgreSQL and MySQL are unaffected.
Op.json(path) nested under the column (not aimport { Op } from 'ts-prorm-orm';
await Product.findAll({ where: { attributes: { [Op.json('$.colour')]: 'red' } } });UnsupportedWhereError ("the operatorUnsupportedWhereError before anything is sent,UnsupportedWhereError before anything is sent.Op.contains, Op.keyExists,Op.jsonHasKey, Op.jsonPathExists and the PostgreSQL @> / -> / #>BLOB('tiny' | 'medium' | 'long'), TINYBLOB(), MEDIUMBLOB(),LONGBLOB(), BINARY(length), VARBINARY(length).ENUM(...values) renders a real ENUM on MySQL/MariaDB, aTEXT CHECK(col IN (...)) on SQLite and a VARCHAR(255) CHECK (col IN (...))const pg = prorm.getDialectInstance();
await pg.createEnumType('user_role', ['admin', 'user'], { ifNotExists: true });
prorm.define('User', {
role: { type: 'user_role' as any, defaultValue: 'user' },
});dropType(name, { ifExists, cascade }) removes it again. A plain string is| Type | Available on |
|---|---|
ARRAY(subtype) | PostgreSQL (INTEGER[]), degrades elsewhere |
LIST(subtype), STRUCT(fields), MAP(key, value), UNION(...) | DuckDB / ClickHouse / Databricks-style nested types |
SET(...values) | MySQL/MariaDB |
RANGE(subtype) | PostgreSQL range types (int4range, tstzrange, …) |
INET(), INET6(), CIDR(), MACADDR() — PostgreSQL native, text elsewhere.GEOMETRY(type?, srid?), GEOGRAPHY(type?, srid?), plus the concrete shapesPOINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING,MULTIPOLYGON, GEOMETRYCOLLECTION.{ location: { type: DataTypes.GEOGRAPHY('POINT', 4326) } }await prorm.createExtension('postgis').VIRTUAL() and VIRTUALSTRING() declare an attribute that has no databasegetDataTypeSql() returns an empty string and the column isCREATE TABLE and from every INSERT/UPDATE. The value isget. Seeconst attributes = {
firstName: { type: DataTypes.STRING(50) },
lastName: { type: DataTypes.STRING(50) },
fullName: {
type: DataTypes.VIRTUAL(),
get(this: { firstName: string; lastName: string }) {
return `${this.firstName} ${this.lastName}`;
},
},
};const Line = prorm.define('Line', {
sku: DataTypes.STRING(64),
price: DataTypes.DECIMAL(10, 2),
inStock: { type: DataTypes.BOOLEAN(), defaultValue: true },
attributes: DataTypes.JSON(),
at: DataTypes.DATE(),
});
await prorm.sync();
await Line.create({ sku: 'A', price: '19.99', attributes: { colour: 'red' }, at: new Date('2026-03-01T09:30:00Z') });
const row = await Line.findOne({ where: { sku: 'A' } });
typeof row.price; // 'number' (19.99; sent as '19.99')
row.inStock; // true (stored as 1)
row.attributes.colour; // 'red' (stored as '{"colour":"red"}')
row.at instanceof Date; // true (stored as an ISO string)| Declared type | Coerced to |
|---|---|
INTEGER, SMALLINT, MEDIUMINT, TINYINT | number |
BIGINT | number, or left as BigInt when the driver returns one |
FLOAT, DOUBLE, DECIMAL, REAL | number — so a DECIMAL(20,4) beyond 15–16 significant digits loses precision in JavaScript |
BOOLEAN | true / false (from 1/0/'1'/'0') |
DATE, DATEONLY | Date |
JSON, JSONB | parsed object / array; unparseable text is returned verbatim |
NaN or anInferAttributes helpers keep the two in step:import { Model, InferAttributes, InferCreationAttributes, CreationOptional } from 'ts-prorm-orm';
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
declare id: CreationOptional<number>; // optional on create, present on read
declare email: string;
declare bio: string | null;
}typeMapping: 'legacy' vs 'modern'