Skip to main content

Overview

Drizzle’s custom type API allows you to create specialized column types that map between TypeScript types and database types. This is useful for:
  • Domain-specific types: Coordinates, currency, encrypted strings
  • Custom serialization: JSON with validation, compressed data
  • Type transformations: Automatic parsing and formatting
  • Database extensions: PostGIS, vector types, custom SQL types

Creating Custom Types

Basic Custom Type

Use the customType() function to define a new column type:
import { customType } from 'drizzle-orm/pg-core';

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

// Use in schema
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  bio: customText('bio'),
});

Type with Data Transformation

Map between JavaScript and database representations:
import { customType } from 'drizzle-orm/pg-core';

// Store JSON with automatic serialization
const jsonb = customType<{ data: any }>({
  dataType() {
    return 'jsonb';
  },
  toDriver(value: any): string {
    return JSON.stringify(value);
  },
  fromDriver(value: string): any {
    return JSON.parse(value);
  },
});

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  metadata: jsonb('metadata'),
});

// Usage
await db.insert(users).values({
  metadata: { theme: 'dark', locale: 'en' }, // Automatically stringified
});

const result = await db.select().from(users);
console.log(result[0].metadata.theme); // Automatically parsed

Type Parameters

Data Type

Define the TypeScript type for your column:
interface Coordinate {
  lat: number;
  lng: number;
}

const point = customType<{ data: Coordinate }>({
  dataType() {
    return 'point';
  },
  toDriver(value: Coordinate): string {
    return `(${value.lat},${value.lng})`;
  },
  fromDriver(value: string): Coordinate {
    const [lat, lng] = value
      .replace(/[()]/g, '')
      .split(',')
      .map(Number);
    return { lat, lng };
  },
});

export const locations = pgTable('locations', {
  id: serial('id').primaryKey(),
  coords: point('coords'),
});

// Type-safe usage
await db.insert(locations).values({
  coords: { lat: 40.7128, lng: -74.0060 }, // NYC
});

Driver Data Type

Specify the database driver’s representation:
const encrypted = customType<{
  data: string;
  driverData: Buffer;
}>({
  dataType() {
    return 'bytea';
  },
  toDriver(value: string): Buffer {
    // Encrypt the string
    return Buffer.from(encrypt(value));
  },
  fromDriver(value: Buffer): string {
    // Decrypt the buffer
    return decrypt(value.toString());
  },
});

Config Type

Add configuration parameters:
const varchar = customType<{
  data: string;
  config: { length: number };
  configRequired: true;
}>({
  dataType(config) {
    return `varchar(${config.length})`;
  },
});

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  username: varchar('username', { length: 50 }),
  bio: varchar('bio', { length: 500 }),
});

Real-World Examples

Currency Type

Handle monetary values with precision:
import { customType } from 'drizzle-orm/pg-core';

interface Money {
  amount: number;
  currency: string;
}

const money = customType<{ data: Money }>({
  dataType() {
    return 'jsonb';
  },
  toDriver(value: Money): string {
    // Store as cents to avoid floating point issues
    return JSON.stringify({
      amount: Math.round(value.amount * 100),
      currency: value.currency,
    });
  },
  fromDriver(value: string): Money {
    const parsed = JSON.parse(value);
    return {
      amount: parsed.amount / 100,
      currency: parsed.currency,
    };
  },
});

export const products = pgTable('products', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  price: money('price').notNull(),
});

// Usage
await db.insert(products).values({
  name: 'Premium Plan',
  price: { amount: 29.99, currency: 'USD' },
});

Date Range Type

PostgreSQL date ranges:
import { customType } from 'drizzle-orm/pg-core';

interface DateRange {
  start: Date;
  end: Date;
}

const daterange = customType<{ data: DateRange }>({
  dataType() {
    return 'daterange';
  },
  toDriver(value: DateRange): string {
    const start = value.start.toISOString().split('T')[0];
    const end = value.end.toISOString().split('T')[0];
    return `[${start},${end}]`;
  },
  fromDriver(value: string): DateRange {
    const match = value.match(/\[(.+?),(.+?)\]/);
    if (!match) throw new Error('Invalid daterange format');
    return {
      start: new Date(match[1]),
      end: new Date(match[2]),
    };
  },
});

export const bookings = pgTable('bookings', {
  id: serial('id').primaryKey(),
  period: daterange('period').notNull(),
});

PostGIS Geography Type

Work with geospatial data:
import { customType } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

interface GeoPoint {
  type: 'Point';
  coordinates: [number, number]; // [longitude, latitude]
}

const geography = customType<{ data: GeoPoint }>({
  dataType() {
    return 'geography(Point, 4326)';
  },
  toDriver(value: GeoPoint) {
    return sql`ST_GeogFromGeoJSON(${JSON.stringify(value)})`;
  },
  fromDriver(value: any): GeoPoint {
    if (typeof value === 'string') {
      return JSON.parse(value);
    }
    return value;
  },
});

export const places = pgTable('places', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  location: geography('location').notNull(),
});

// Usage
await db.insert(places).values({
  name: 'Central Park',
  location: {
    type: 'Point',
    coordinates: [-73.965355, 40.782865],
  },
});

// Query with distance
const nearby = await db
  .select()
  .from(places)
  .where(
    sql`ST_DWithin(
      ${places.location},
      ST_GeogFromGeoJSON(${JSON.stringify({
        type: 'Point',
        coordinates: [-73.97, 40.78],
      })}),
      1000
    )`
  );

Validated Email Type

Add validation during serialization:
import { customType } from 'drizzle-orm/pg-core';

function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

const email = customType<{ data: string }>({
  dataType() {
    return 'text';
  },
  toDriver(value: string): string {
    if (!isValidEmail(value)) {
      throw new Error(`Invalid email: ${value}`);
    }
    return value.toLowerCase();
  },
});

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: email('email').notNull(),
});

// Throws error if invalid
await db.insert(users).values({
  email: '[email protected]', // Stored as '[email protected]'
});

UUID v7 Type

Implement UUID v7 with timestamp ordering:
import { customType } from 'drizzle-orm/pg-core';
import { uuidv7 } from 'uuidv7';

const uuidv7Type = customType<{
  data: string;
  default: true;
}>({
  dataType() {
    return 'uuid';
  },
  toDriver(value: string): string {
    return value;
  },
  fromDriver(value: string): string {
    return value;
  },
});

export const users = pgTable('users', {
  id: uuidv7Type('id').primaryKey().$defaultFn(() => uuidv7()),
  name: text('name').notNull(),
});

Database-Specific Types

PostgreSQL

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

// Array type
const textArray = customType<{ data: string[] }>({
  dataType() {
    return 'text[]';
  },
  toDriver(value: string[]): string[] {
    return value;
  },
});

// HSTORE type
const hstore = customType<{ data: Record<string, string> }>({
  dataType() {
    return 'hstore';
  },
  toDriver(value: Record<string, string>): string {
    return Object.entries(value)
      .map(([k, v]) => `"${k}"=>"${v}"`)
      .join(',');
  },
  fromDriver(value: string): Record<string, string> {
    const result: Record<string, string> = {};
    const pairs = value.match(/"([^"]+)"=>"([^"]+)"/g) || [];
    for (const pair of pairs) {
      const match = pair.match(/"([^"]+)"=>"([^"]+)"/);
      if (match) result[match[1]] = match[2];
    }
    return result;
  },
});

MySQL

import { customType } from 'drizzle-orm/mysql-core';

// SET type
const set = customType<{
  data: string[];
  config: { values: readonly string[] };
  configRequired: true;
}>({
  dataType(config) {
    return `set(${config.values.map(v => `'${v}'`).join(',')})`;
  },
  toDriver(value: string[]): string {
    return value.join(',');
  },
  fromDriver(value: string): string[] {
    return value.split(',').filter(Boolean);
  },
});

export const users = mysqlTable('users', {
  id: int('id').primaryKey(),
  permissions: set('permissions', {
    values: ['read', 'write', 'delete'] as const,
  }),
});

SQLite

import { customType } from 'drizzle-orm/sqlite-core';

// Boolean stored as integer
const boolean = customType<{ data: boolean }>({
  dataType() {
    return 'integer';
  },
  toDriver(value: boolean): number {
    return value ? 1 : 0;
  },
  fromDriver(value: number): boolean {
    return value === 1;
  },
});

// Date stored as ISO string
const timestamp = customType<{ data: Date }>({
  dataType() {
    return 'text';
  },
  toDriver(value: Date): string {
    return value.toISOString();
  },
  fromDriver(value: string): Date {
    return new Date(value);
  },
});

Advanced Patterns

Configurable Precision

const decimal = customType<{
  data: number;
  config: { precision: number; scale: number };
  configRequired: true;
}>({
  dataType(config) {
    return `decimal(${config.precision},${config.scale})`;
  },
  toDriver(value: number): string {
    return value.toFixed(this.config?.scale || 2);
  },
  fromDriver(value: string): number {
    return parseFloat(value);
  },
});

export const products = pgTable('products', {
  id: serial('id').primaryKey(),
  price: decimal('price', { precision: 10, scale: 2 }),
  weight: decimal('weight', { precision: 8, scale: 3 }),
});

NotNull by Default

const customSerial = customType<{
  data: number;
  notNull: true;
  default: true;
}>({
  dataType() {
    return 'serial';
  },
});

export const items = pgTable('items', {
  id: customSerial('id').primaryKey(),
  // id is automatically NOT NULL
});

With Validation Schema

Integrate with Zod or other validators:
import { z } from 'zod';
import { customType } from 'drizzle-orm/pg-core';

const phoneSchema = z.string().regex(/^\+?[1-9]\d{1,14}$/);

const phone = customType<{ data: string }>({
  dataType() {
    return 'text';
  },
  toDriver(value: string): string {
    const parsed = phoneSchema.safeParse(value);
    if (!parsed.success) {
      throw new Error(`Invalid phone number: ${value}`);
    }
    return value;
  },
});

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  phone: phone('phone'),
});

Best Practices

1

Keep transformations simple

Complex logic should be in your application layer, not in type mappings.
2

Handle null values explicitly

Check for null in fromDriver and toDriver methods.
3

Use appropriate database types

Choose the database type that best matches your data semantics.
4

Validate in toDriver

Catch invalid data before it reaches the database.
5

Document your custom types

Add JSDoc comments explaining the type’s behavior and constraints.

Type Safety

Custom types maintain full TypeScript inference:
const users = pgTable('users', {
  id: serial('id').primaryKey(),
  location: point('location'),
});

// TypeScript infers the type
type User = typeof users.$inferSelect;
// { id: number; location: Coordinate | null }

type NewUser = typeof users.$inferInsert;
// { id?: number; location?: Coordinate | null }

// Type-safe insert
await db.insert(users).values({
  location: { lat: 40.7128, lng: -74.0060 },
  // location: { x: 1, y: 2 }, // TypeScript error!
});
Export your custom types from a shared module to reuse them across multiple tables and maintain consistency.

Common Pitfalls

Avoid throwing errors in fromDriver as it will break queries. Return a safe default or handle errors gracefully.
// ✗ BAD: Throwing in fromDriver
const badType = customType<{ data: number }>({
  dataType() { return 'text'; },
  fromDriver(value: string): number {
    const num = parseInt(value);
    if (isNaN(num)) {
      throw new Error('Invalid number'); // Will break queries!
    }
    return num;
  },
});

// ✓ GOOD: Handle errors gracefully
const goodType = customType<{ data: number }>({
  dataType() { return 'text'; },
  fromDriver(value: string): number {
    const num = parseInt(value);
    return isNaN(num) ? 0 : num; // Return safe default
  },
});

Build docs developers (and LLMs) love