> ## Content Index
> Fetch the complete content index at: https://madewithlove.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# Branded types in TypeScript: when string just isn’t enough
- URL: https://madewithlove.com/blog/branded-types-in-typescript-when-string-just-isnt-enough/
- Published: 2025-04-30T07:36:10.000Z
- Updated: 2025-04-30T07:36:10.000Z
- Description: Discover how branded types can improve your TypeScript code quality, make your intent clearer, and catch bugs before they even compile.
- Author: Geoffrey Dhuyvetters
- Tags: Engineering, JavaScript, TypeScript, Frontend

Ever spent way too much time [**debugging**](https://madewithlove.com/blog/follow-these-4-steps-to-take-your-debugging-skills-to-the-next-level/) 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

```javascript
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:

```javascript
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](https://madewithlove.com/blog/typescript-vue-3-and-strongly-typed-props/) *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.

```javascript
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, “UserId”>;
type ProductId = Brand<string, “ProductId”>;
```

Let's adapt the schema

```javascript
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

```javascript
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.