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-70867728950eBut 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_0fhypnqpeb8tft2sbggsvjh58eUse 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 = 'u' | 'org' | 'file' | 'transaction'
export function generateId<T extends IdPrefixes>(prefix: T) { return typeidUnboxed(prefix)}
export type UserId = TypeId<'u'>export type OrgId = TypeId<'org'>export type FileId = TypeId<'file'>export type TransactionId = TypeId<'t'>Use with Zod
Section titled: Use with ZodYou can choose whether or not you actually want to validate the shape of the id or just validate that it’s a string. Chances are that your application code doesn’t truly care what the shape of the id, that’s for human benefit.
Just validate as a string
Section titled: Just validate as a stringz.object({ userId: z.string().transform((val) => val as UserId),})Validate the shape of the type id unboxed
Section titled: Validate the shape of the type id unboxedFirst create a utility. Instead of extends string you could use extends IdPrefixes if you have created that string union type (as shown above).
import {fromString, type TypeId} from 'typeid-js'
export function typeIdSchema<const P extends string>(prefix: P) { return z.custom<TypeId<P>>( (val) => { if (typeof val !== 'string') return false try { fromString(val, prefix) // throws on wrong prefix / malformed suffix return true } catch { return false } }, {message: `Invalid TypeID (expected prefix "${prefix}")`}, )}z.object({ userId: typeIdSchema('u'),})