little cubes

Your IDs need better DX

They can be sortable, selectable, distinguishable, and type safe.

The most common ID that I’ve seen used in software is a GUID (aka UUID v4):

e2aca937-bf1e-4224-9546-51b317abaced
ced452f2-a194-40f5-9ad4-9eda83ec558f
foo.com/0f8fad5b-d9cb-469f-a165-70867728950e

But these have a number of shortcomings:

  • They’re long and ugly to look at (e.g. in urls)
  • Go ahead and double click on that third one to select the it…oh wait
  • They’re completely random which makes them unsortable and generally suffer from poor database locality if used as a primary key
  • You can’t tell an org id apart from a user id apart from a transaction id. They’re all interchangeable and when someone sends you an id you have no idea what it’s for without more context.
  • There is obviously no type safety preventing you from passing a user id where a file id is required

TypeIDs solve each of those problems:

user_2x4y6z8a0b1c2d3e4f5g6h7j8k
└──┘ └────────────────────────┘
type uuid suffix (base32)
  • Thoughtful encoding: the base32 encoding is URL safe, case-insensitive, avoids ambiguous characters, can be selected for copy-pasting by double-clicking, and is a more compact encoding than the traditional hex encoding used by UUIDs (26 characters vs 36 characters).
  • K-Sortable: TypeIDs are K-sortable and can be used as the primary key in a database while ensuring good locality. Compare to entirely random global ids, like UUIDv4, that generally suffer from poor database locality.
  • Type-safe: you can’t accidentally use a user ID where a post ID is expected. When debugging, you can immediately understand what type of entity a TypeID refers to thanks to the type prefix.

Here are the same three guids from earlier as typeids, what a difference!

user_6eth9f58cm83tsnn4yva1yrncf
org_6eth9f58cm83tsnn4yva1yrncf
foo.com/file_0fhypnqpeb8tft2sbggsvjh58e

By default, the TypeScript implementation of TypeId, typeid-js, uses class instances. I’m not a big fan of that because it requires a serialization process before and after fetching data from your database.

Thankfully, the clever people behind typeid-js have thought of this and also provided us with typeid-unboxed. At runtime it’s just a string, but at compile time is still distinguishable based on its prefix:

import {typeidUnboxed, type TypeId} from 'typeid-js'
function doSomethingWithOrgId(id: TypeId<'org'>) {
/* ...*/
}
const userId = typeidUnboxed('user')
// TypeId (unboxed) is just a (fancy) string and can be used as one with no extra steps
const justAString: string = userId
doSomethingWithOrgId(userId)                      
Argument of type 'TypeId<"user">' is not assignable to parameter of type 'TypeId<"org">'.

I like to wrap typeidUnboxed in a function just to strongly type the list of ids that my app uses:

import {typeidUnboxed, type TypeId} from 'typeid-js'
type IdPrefixes = 'user' | 'org' | 'file' | 'transaction'
function generateId<T extends IdPrefixes>(prefix: T) {
return typeidUnboxed(prefix)
}