Ever spent way too much time debugging something that turned out to be a tiny, obvious mistake?
The Problem with Plain Strings
Let's assume we have these (very simple) Drizzle schema's
export const users = mysqlTable("user", {
id: varchar("id", { length: 255 })
.primaryKey()
});
export const products = mysqlTable("product", {
id: varchar("id", { length: 255 })
.primaryKey()
});
And a function that fetches a user:
function getUser(userId: string) {
console.log(`Fetching user ${userId}`);
}
getUser(product.id);
Everything looks fine. No warnings, no errors. But the request doesn’t return a user. Maybe it fails silently, maybe it returns an empty object, or maybe it even throws an unexpected error. Either way, you’re now staring at logs, checking API responses, and wondering what’s wrong.
Then, after wasting time debugging the backend, you realize: you passed a product.id instead of a user.id. The function didn’t care—they were both strings.
Branded Types to the Rescue
What if TypeScript knew that a userId is different from a productId? That’s where branded types come in. They let us distinguish between values that share the same primitive type but have different meanings.
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, “UserId”>;
type ProductId = Brand<string, “ProductId”>;
Let's adapt the schema
export type UserId = Brand<string, "UserId">;
export const users = mysqlTable("user", {
id: varchar("id", { length: 255 })
.primaryKey()
.$type<UserId>()
});
export type ProductId = Brand<string, "ProductId">;
export const products = mysqlTable("product", {
id: varchar("id", { length: 255 })
.primaryKey()
.$type<ProductId>()
});
Now, let’s fix our function
function getUser(userId: UserId) {
console.log(`Fetching user ${userId}`);
}
getUser(product.id);
// Type error: Argument of type ‘ProductId’ is not assignable to parameter of type ‘UserId’.
Catching Bugs Before They Happen
With branded types, TypeScript stops these mistakes at the source—before they turn into hours of debugging. It’s a small adjustment with a big payoff: clearer intent, safer APIs, and one less category of bugs to worry about.
Member discussion