Custom types

Custom types let you define your own column types with custom SQL data types and JS/DB value transforms

PostgreSQL exposes customType from drizzle-orm/pg-core.

Examples

The best way to see how customType definition is working is to check how existing data types could be defined using customType function from Drizzle ORM.

Integer

import { customType, pgTable } from 'drizzle-orm/pg-core';

const customInteger = customType<{ data: number; }>(
  {
    dataType() {
      return 'integer';
    },
  },
);

export const users = pgTable("users", {
  id: customInteger(),
});

Text

import { customType, pgTable } from 'drizzle-orm/pg-core';

const customText = customType<{ data: string }>({
  dataType() {
    return 'text';
  },
});

export const users = pgTable('users', {
  name: customText(),
});

Timestamp

import { customType, pgTable } from 'drizzle-orm/pg-core';

const customTimestamp = customType<{
  data: Date;
  driverData: string;
  config: { withTimezone: boolean; precision?: number };
  configRequired: false;
}>({
  dataType(config) {
    const precision = typeof config?.precision !== 'undefined' ? ` (${config.precision})` : '';
    return `timestamp${precision}${config?.withTimezone ? ' with time zone' : ''}`;
  },
  fromDriver(value: string): Date {
    return new Date(value);
  },
  codec: (config) => {
    if (config?.withTimezone) return 'timestamp with time zone';

    return 'timestamp';
  },
});

export const users = pgTable('users', {
  createdAt: customTimestamp('created_at'),
});

Usage for all types will be same as defined functions in Drizzle ORM. For example:

const usersTable = pgTable('users', {
  id: customInteger().primaryKey(),
  name: customText().notNull(),
  createdAt: customTimestamp('created_at', { withTimezone: true }).notNull()
    .default(sql`now()`),
});

Methods and Generic types


TypeRequiredDescription
dataYesThe TypeScript type for the column after selecting/inserting.
driverDataNoThe type the database driver accepts for this data type.
driverOutputNoDeprecated — use codecs instead (bypasses JSON codecs if used). The type the driver returns, if different from driverData.
jsonDataNoDeprecated — use codecs instead (bypasses JSON codecs if used). The type returned when aggregated to JSON.
configNoConfig object type for dataType generation (e.g. { length: number }).
configRequiredNoWhether the config argument is required. Default false.
notNullNoSet to true if the custom type should be notNull by default.
defaultNoSet to true if the custom type has a default value.

MethodRequiredDescription
dataTypeYesDefines the SQL type string. If database data type needs additional params you can use them from config param
toDriverNoTransform inputs from desired to be used in code format to one suitable for driver
fromDriverNoTransform data returned by driver to desired column’s output format
codecNoWhich driver codec to use (see Codecs section).
fromJsonNoDeprecated — use codec instead. Transforms value from JSON context.
forJsonSelectNoDeprecated — use codec instead. Modify selection of column inside JSON functions

TS-doc for type definitions

You can check ts-doc for types, param definition and examples

Expand details

interface CustomTypeValues {
  /**
   * Required type for custom column, that will infer proper type model
   *
   * Examples:
   *
   * If you want your column to be `string` type after selecting/or on inserting - use `data: string`. Like `text`, `varchar`
   *
   * If you want your column to be `number` type after selecting/or on inserting - use `data: number`. Like `integer`
   */
  data: unknown;
  /**
   * Type helper, that represents what type database driver is accepting for specific database data type
   */
  driverData?: unknown;
  /**
   * @deprecated Use codecs instead
   *
   * Type helper, that represents what type database driver is returning for specific database data type
   *
   * Needed only in case driver's output and input for type differ
   *
   * Defaults to {@link driverData}
   */
  driverOutput?: unknown;
  /**
   * @deprecated Use codecs instead
   *
   * Type helper, that represents what type field returns after being aggregated to JSON
   */
  jsonData?: unknown;
  /**
   * What config type should be used for {@link CustomTypeParams} `dataType` generation
   */
  config?: Record<string, any>;
  /**
   * Whether the config argument should be required or not
   * @default false
   */
  configRequired?: boolean;
  /**
   * If your custom data type should be notNull by default you can use `notNull: true`
   *
   * @example
   * const customSerial = customType<{ data: number, notNull: true, default: true }>({
   * 	  dataType() {
   * 	    return 'serial';
   *    },
   * });
   */
  notNull?: boolean;
  /**
   * If your custom data type has default you can use `default: true`
   *
   * @example
   * const customSerial = customType<{ data: number, notNull: true, default: true }>({
   * 	  dataType() {
   * 	    return 'serial';
   *    },
   * });
   */
  default?: boolean;
}
interface CustomTypeParams<T extends CustomTypeValues> {
  /**
   * Database data type string representation, that is used for migrations
   * @example
   * ```
   * `jsonb`, `text`
   * ```
   *
   * If database data type needs additional params you can use them from `config` param
   * @example
   * ```
   * `varchar(256)`, `numeric(2,3)`
   * ```
   *
   * To make `config` be of specific type please use config generic in {@link CustomTypeValues}
   *
   * @example
   * Usage example
   * ```
   *   dataType() {
   *     return 'boolean';
   *   },
   * ```
   * Or
   * ```
   *   dataType(config) {
   * 	   return typeof config.length !== 'undefined' ? `varchar(${config.length})` : `varchar`;
   * 	 }
   * ```
   */
  dataType: (config: T['config'] | (Equal<T['configRequired'], true> extends true ? never : undefined)) => string;
  /**
   * Optional mapping function, that is used to transform inputs from desired to be used in code format to one suitable for driver
   * @example
   * For example, when using jsonb we need to map JS/TS object to string before writing to database
   * ```
   * toDriver(value: TData): string {
   * 	 return JSON.stringify(value);
   * }
   * ```
   */
  toDriver?: (value: T['data']) => T['driverData'] | SQL;
  /**
   * Optional mapping function, that is used for transforming data returned by driver to desired column's output format
   * @example
   * For example, when using timestamp we need to map string Date representation to JS Date
   * ```
   * fromDriver(value: string): Date {
   * 	return new Date(value);
   * }
   * ```
   *
   * It'll cause the returned data to change from:
   * ```
   * {
   * 	customField: "2025-04-07T03:25:16.635Z";
   * }
   * ```
   * to:
   * ```
   * {
   * 	customField: new Date("2025-04-07T03:25:16.635Z");
   * }
   * ```
   */
  fromDriver?: (value: 'driverOutput' extends keyof T ? T['driverOutput'] : T['driverData']) => T['data'];
  /**
   * @deprecated Use codecs instead; bypasses JSON codecs if used
   *
   * Optional mapping function, that is used for transforming data returned by transofmed to JSON in database data to desired format
   *
   * Used by [relational queries](https://orm.drizzle.team/docs/rqb)
   *
   * Defaults to {@link fromDriver} function
   * @example
   * For example, when querying bigint column via [RQB](https://orm.drizzle.team/docs/rqb) or JSON functions, the result field will be returned as it's string representation, as opposed to bigint from regular query
   * To handle that, we need a separate function to handle such field's mapping:
   * ```
   * fromJson(value: string): bigint {
   * 	return BigInt(value);
   * },
   * ```
   *
   * It'll cause the returned data to change from:
   * ```
   * {
   * 	customField: "5044565289845416380";
   * }
   * ```
   * to:
   * ```
   * {
   * 	customField: 5044565289845416380n;
   * }
   * ```
   */
  fromJson?: (value: T['jsonData']) => T['data'];
  /**
   * @deprecated Use codecs instead; bypasses JSON codecs if used
   *
   * Optional selection modifier function, that is used for modifying selection of column inside JSON functions
   *
   * Additional mapping that could be required for such scenarios can be handled using {@link fromJson} function
   *
   * Used by [relational queries](https://orm.drizzle.team/docs/rqb)
   *
   * Following types are being casted to text by default: `bytea`, `geometry`, `timestamp`, `numeric`, `bigint`
   * @example
   * For example, when using bigint we need to cast field to text to preserve data integrity
   * ```
   * forJsonSelect(identifier: SQL, sql: SQLGenerator, arrayDimensions?: number): SQL {
   * 	return sql`${identifier}::text`
   * },
   * ```
   *
   * This will change query from:
   * ```
   * SELECT
   * 	row_to_json("t".*)
   * 	FROM
   * 	(
   * 		SELECT
   * 		"table"."custom_bigint" AS "bigint"
   * 		FROM
   * 		"table"
   * 	) AS "t"
   * ```
   * to:
   * ```
   * SELECT
   * 	row_to_json("t".*)
   * 	FROM
   * 	(
   * 		SELECT
   * 		"table"."custom_bigint"::text AS "bigint"
   * 		FROM
   * 		"table"
   * 	) AS "t"
   * ```
   *
   * Returned by query object will change from:
   * ```
   * {
   * 	bigint: 5044565289845416000; // Partial data loss due to direct conversion to JSON format
   * }
   * ```
   * to:
   * ```
   * {
   * 	bigint: "5044565289845416380"; // Data is preserved due to conversion of field to text before JSON-ification
   * }
   * ```
   */
  forJsonSelect?: (identifier: SQL, sql: SQLGenerator, arrayDimensions?: number) => SQL;
  /**
   * Select which column type codec will be used for this column
   */
  codec?: PostgresColumnType | undefined | ((config: T['config'] | (Equal<T['configRequired'], true> extends true ? never : undefined)) => PostgresColumnType | undefined);
}

Transform layers and execution order

Custom types have two transform layers: column-level (toDriver/fromDriver) and driver-level (codecs). Both run in a specific order. You can read more on how to use codecs with customTypes here



Use both (toDriver/fromDriver and codecs) when you need driver-level normalization AND column-level transformation:

// DB stores as bigint, driver returns string, codec converts to BigInt,
// but you want a number in your app code
const myColumn = customType<{ data: number; driverData: string }>({
  dataType() { return 'bigint'; },
  codec: 'bigint',           // driver "123" → BigInt(123n)
  fromDriver(value) {        // BigInt(123n) → Number(123)
    return Number(value);
  },
});

But often you can simplify by choosing the right codec variant:

// Same result, no fromDriver needed
const myColumn = customType<{ data: number; driverData: string }>({
  dataType() { return 'bigint'; },
  codec: 'bigint:number',    // driver "123" → Number(123) directly
});