Custom Types
Register your own types for automatic serialization
fatline handles Date, Set, Map, BigInt, and other built-in types automatically. For your own types—Decimal, Money, domain objects—you register them once and they serialize everywhere.
Basic Registration
import { registerType } from 'fatline'
import Decimal from 'decimal.js'
registerType({
name: 'Decimal',
test: (v): v is Decimal => v instanceof Decimal,
encode: (v) => v.toString(),
decode: (v) => new Decimal(v),
})That's it. Now every Decimal in your data serializes and deserializes automatically:
serialize({ price: new Decimal('19.99') })
// → {"price":["~",47821,"19.99"]}
deserialize(json)
// → { price: Decimal { ... } }How IDs Work
Type IDs 0-20 are reserved for built-in types. Custom types get IDs 21+.
Auto-assigned (recommended): fatline generates a stable ID from the type name using FNV-1a hash:
import { registerType, getTypeId } from 'fatline'
registerType({
name: 'Decimal', // ID derived from 'Decimal'
test: (v): v is Decimal => v instanceof Decimal,
encode: (v) => v.toString(),
decode: (v) => new Decimal(v),
})
getTypeId('Decimal') // 47821 (stable, deterministic)Manual ID: For compatibility with existing serialized data:
registerType({
id: 25, // Explicit ID
name: 'Money',
test: (v): v is Money => v instanceof Money,
encode: (v) => v.cents,
decode: (v) => new Money(v),
})Common Patterns
Decimal.js / Prisma Decimal
import Decimal from 'decimal.js'
registerType({
name: 'Decimal',
test: (v): v is Decimal => v instanceof Decimal,
encode: (v) => v.toString(),
decode: (v) => new Decimal(v),
})Money / Currency
class Money {
constructor(public cents: number, public currency: string) {}
}
registerType({
name: 'Money',
test: (v): v is Money => v instanceof Money,
encode: (v) => ({ cents: v.cents, currency: v.currency }),
decode: (v) => new Money(v.cents, v.currency),
})Temporal (Stage 3 Proposal)
registerType({
name: 'Temporal.Instant',
test: (v): v is Temporal.Instant => v instanceof Temporal.Instant,
encode: (v) => v.epochNanoseconds.toString(),
decode: (v) => Temporal.Instant.fromEpochNanoseconds(BigInt(v)),
})Luxon DateTime
import { DateTime } from 'luxon'
registerType({
name: 'DateTime',
test: (v): v is DateTime => DateTime.isDateTime(v),
encode: (v) => v.toISO(),
decode: (v) => DateTime.fromISO(v),
})Branded Types / Opaque Types
For TypeScript branded types, you typically don't need custom registration since they're just primitives at runtime:
type UserId = string & { readonly __brand: 'UserId' }
// No registration needed—it's just a string at runtime
serialize({ id: 'user_123' as UserId })Registration Timing
Register types before any serialize/deserialize calls:
// ✓ Good: Register at app startup
import { registerType } from 'fatline'
import './types/decimal' // Registers Decimal type
// ✗ Bad: Lazy registration
function getPrice() {
registerType({ ... }) // Too late if data was already serialized elsewhere
}For frameworks, register in your entry point or a shared module that's imported first.
Testing Custom Types
import { serialize, deserialize, registerType } from 'fatline'
registerType({
name: 'Money',
test: (v): v is Money => v instanceof Money,
encode: (v) => v.cents,
decode: (v) => new Money(v),
})
test('Money round-trips', () => {
const original = new Money(1999)
const json = serialize({ price: original })
const restored = deserialize(json)
expect(restored.price).toBeInstanceOf(Money)
expect(restored.price.cents).toBe(1999)
})