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-51b317abacedced452f2-a194-40f5-9ad4-9eda83ec558ffoo.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
TypeID
Section titled: TypeIDTypeIDs 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_6eth9f58cm83tsnn4yva1yrncforg_6eth9f58cm83tsnn4yva1yrncffoo.com/file_0fhypnqpeb8tft2sbggsvjh58e
Use in TypeScript
Section titled: Use in TypeScriptBy 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 stepsconst justAString: string = userId
doSomethingWithOrgId(userId)
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)}