EpicenterHQ

drizzle-orm

3,976
268
# Install this skill:
npx skills add EpicenterHQ/epicenter --skill "drizzle-orm"

Install specific skill from multi-skill repository

# Description

Drizzle ORM patterns for type branding and custom types. Use when working with Drizzle column definitions, branded types, or custom type conversions.

# SKILL.md


name: drizzle-orm
description: Drizzle ORM patterns for type branding and custom types. Use when working with Drizzle column definitions, branded types, or custom type conversions.


Drizzle ORM Guidelines

Use $type() for Branded Strings, Not customType

When you need a column with a branded TypeScript type but no actual data transformation, use $type<T>() instead of customType.

The Rule

If toDriver and fromDriver would be identity functions (x) => x, use $type<T>() instead.

Why

Even with identity functions, customType still invokes mapFromDriverValue on every row:

// drizzle-orm/src/utils.ts - runs for EVERY column of EVERY row
const rawValue = row[columnIndex]!;
const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);

Query 1000 rows with 3 date columns = 3000 function calls doing nothing.

Bad Pattern

// Runtime overhead for identity functions
customType<{ data: DateTimeString; driverParam: DateTimeString }>({
    dataType: () => 'text',
    toDriver: (value) => value, // called on every write
    fromDriver: (value) => value, // called on every read
});

Good Pattern

// Zero runtime overhead - pure type assertion
text().$type<DateTimeString>();

$type<T>() is a compile-time-only type override:

// drizzle-orm/src/column-builder.ts
$type<TType>(): $Type<this, TType> {
  return this as $Type<this, TType>;
}

When to Use customType

Only when data genuinely transforms between app and database:

// JSON: object ↔ string - actual transformation
customType<{ data: UserPrefs; driverParam: string }>({
    toDriver: (value) => JSON.stringify(value),
    fromDriver: (value) => JSON.parse(value),
});

Keep Data in Intermediate Representation

Prefer keeping data serialized (strings) through the system, parsing only at the edges (UI components).

The principle: If data enters serialized and leaves serialized, keep it serialized in the middle. Parse at the edges where you actually need the rich representation.

Example: DateTimeString

Instead of parsing DateTimeString into Temporal.ZonedDateTime at the database layer:

// Bad: parse on every read, re-serialize at API boundaries
customType<{ data: Temporal.ZonedDateTime; driverParam: string }>({
    fromDriver: (value) => fromDateTimeString(value),
});

Keep it as a string until the UI actually needs it:

// Good: string stays string, parse only in date-picker component
text().$type<DateTimeString>();

// In UI component:
const temporal = fromDateTimeString(row.createdAt);
// After edit:
const updated = toDateTimeString(temporal);

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.