惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Register - Security
The Register - Security
V
Visual Studio Blog
aimingoo的专栏
aimingoo的专栏
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
量子位
C
Check Point Blog
博客园 - 【当耐特】
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Engineering at Meta
Engineering at Meta
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
博客园 - Franky
N
Netflix TechBlog - Medium
Webroot Blog
Webroot Blog
A
About on SuperTechFans
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
D
Docker
S
Security Affairs
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
Jina AI
Jina AI
Help Net Security
Help Net Security
L
LangChain Blog
P
Proofpoint News Feed
The Cloudflare Blog
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
Recent Announcements
Recent Announcements
Attack and Defense Labs
Attack and Defense Labs
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
Microsoft Security Blog
Microsoft Security Blog
H
Heimdal Security Blog
P
Proofpoint News Feed
O
OpenAI News
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
爱范儿
爱范儿
Security Archives - TechRepublic
Security Archives - TechRepublic

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
TypeScript Branded Types: Stop Passing the Wrong ID at Compile Time
The Practica · 2026-06-17 · via The Practical Developer

A user’s payment was charged to someone else’s order. Not a SQL injection. Not a race condition. The developer called findOrder(userId) instead of findOrder(orderId), and TypeScript said nothing because both arguments were string.

This bug is not rare. It happens in every codebase that grows beyond a handful of files. TypeScript’s structural type system treats any string as compatible with any other string. If your domain has a UserId, an OrderId, and a ProductId and all three are plain strings, the compiler cannot distinguish between them. A function that expects an OrderId will happily accept a UserId, and the mistake surfaces at runtime as a 404, a wrong charge, or a corrupted record.

This post shows you how to fix that with branded types: a zero-cost abstraction that turns TypeScript’s structural type system into a nominally typed one for the identifiers that matter in your domain. You will get a compile-time error when you pass a UserId where an OrderId belongs, and you will pay exactly zero bytes at runtime.

Why plain types are not enough

Here is the problem in its simplest form:

type UserId = string;
type OrderId = string;

async function getOrder(orderId: OrderId): Promise<Order> { /* ... */ }
async function getUser(userId: UserId): Promise<User> { /* ... */ }

// This compiles. It should not.
const user = await getUser(orderId); // orderId is an OrderId, not a UserId

TypeScript sees string on both sides and calls it a day. It does not care that your domain concepts are different. It only checks the shape, and the shape of UserId and OrderId is identical: a string.

Type aliases are documentation, not enforcement. They help you read code, but they do not prevent you from writing bugs.

You could wrap every ID in a class:

class UserId { constructor(public readonly value: string) {} }
class OrderId { constructor(public readonly value: string) {} }

Now the compiler catches the mistake. But every function call becomes getUser(new UserId(someString)), every comparison requires .value, and every serialization step needs custom JSON handling. The runtime cost adds up. The ergonomic tax is high enough that most teams give up after a week.

Branded types give you the safety of nominal typing with none of the runtime overhead.

The branded type pattern

A branded type uses an intersection of the underlying type with a phantom type that only exists at compile time:

declare const UserIdBrand: unique symbol;
type UserId = string & { [UserIdBrand]: true };

declare const OrderIdBrand: unique symbol;
type OrderId = string & { [OrderIdBrand]: true };

The unique symbol declarations and the intersection with { [UserIdBrand]: true } create a type that is still a string at runtime (no wrapper object, no class instance) but is distinct at the type level. The declare const ... is never emitted in the compiled JavaScript. It disappears entirely.

Now this is a compile error:

async function getOrder(orderId: OrderId): Promise<Order> { /* ... */ }

declare const orderId: OrderId;
declare const userId: UserId;

getOrder(userId); // Error: Type 'UserId' is not assignable to type 'OrderId'.

The error message points directly to the bug: you passed a UserId where an OrderId was expected. The compiler saved you from a production incident.

Building a generic brand utility

Writing out the brand declaration for every type gets repetitive. Create a reusable utility:

declare const Brand: unique symbol;
export type Brand<T, B> = T & { [Brand]: B };

Now define your domain types with a single line each:

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
export type ProductId = Brand<string, 'ProductId'>;
export type EmailAddress = Brand<string, 'EmailAddress'>;
export type PhoneNumber = Brand<string, 'PhoneNumber'>;
export type SKU = Brand<string, 'SKU'>;

The second type parameter ('UserId') is the brand name. It does not need to match anything. It just needs to be unique so the types are structurally distinct.

You can brand any primitive, not just strings:

export type UserAge = Brand<number, 'UserAge'>;
export type OrderTotal = Brand<number, 'OrderTotal'>;
export type ProductRating = Brand<number, 'ProductRating'>;
export type PositiveInteger = Brand<number, 'PositiveInteger'>;

A function that expects OrderTotal rejects UserAge at compile time, even though both are numbers.

The guardrail pattern: brand at boundaries, unbox internally

The most important rule of branded types: create branded values at trusted boundaries (API handlers, database queries, constructor functions) and use the raw type internally when you need to.

Here is the pattern:

// Boundary: the only place a UserId is created from user input
function userIdFromString(value: string): UserId {
  if (!/^[a-f0-9]{24}$/.test(value)) {
    throw new Error(`Invalid user ID format: ${value}`);
  }
  return value as unknown as UserId;
}

// Service layer: every function is type-safe
async function getOrdersForUser(userId: UserId): Promise<Order[]> {
  // Internally, cast to string when needed
  const result = await db.query(
    'SELECT * FROM orders WHERE user_id = $1',
    [userId as string]
  );
  return result.rows;
}

The cast value as unknown as UserId is the only unsafe operation. It lives in exactly one place per type: the constructor function. Every consumer of that type from that point on is compile-time safe. Review that one line in code review, and the rest of the codebase is guaranteed correct.

This is the inversion of most type-safety discussions. Instead of making every function validate its inputs (which nobody does consistently), you validate once at the boundary and encode the result in the type system. The compiler enforces the rest.

Real example: refactoring a payment flow

Consider a payment processing pipeline. Before branded types, it looks like this:

async function processPayment(userId: string, orderId: string, amount: number) {
  const user = await findUser(userId);
  const order = await findOrder(orderId);

  if (!user) throw new Error('User not found');
  if (!order) throw new Error('Order not found');

  return chargeCard(user.paymentToken, amount);
}

// Called from a route handler
router.post('/pay', async (req, res) => {
  await processPayment(req.body.userId, req.body.orderId, req.body.amount);
});

Every argument is string. Nothing stops a caller from swapping userId and orderId. Nothing validates that the amount is positive.

With branded types:

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
export type Amount = Brand<number, 'PositiveAmount'>;

// Single trusted boundary per type
const UserId = {
  from: (s: string): UserId => s as unknown as UserId,
};
const OrderId = {
  from: (s: string): OrderId => s as unknown as OrderId,
};
const Amount = {
  from: (n: number): Amount => {
    if (n <= 0 || !Number.isFinite(n)) {
      throw new Error(`Invalid amount: ${n}`);
    }
    return n as unknown as Amount;
  },
};

async function processPayment(
  userId: UserId,
  orderId: OrderId,
  amount: Amount
) {
  const user = await findUser(userId);
  const order = await findOrder(orderId);

  if (!user) throw new Error('User not found');
  if (!order) throw new Error('Order not found');

  return chargeCard(user.paymentToken, amount as number);
}

// Route handler: validate and convert at the boundary
router.post('/pay', async (req, res) => {
  try {
    const userId = UserId.from(req.body.userId);
    const orderId = OrderId.from(req.body.orderId);
    const amount = Amount.from(Number(req.body.amount));

    await processPayment(userId, orderId, amount);
    res.json({ status: 'ok' });
  } catch (err) {
    res.status(400).json({ error: (err as Error).message });
  }
});

Now any call to processPayment where arguments are swapped produces a compile-time error. The validation logic for each type is centralized in one place. The route handler catches formatting errors early and returns a clean 400. The domain logic never needs to re-validate.

Branded IDs in database queries

Real applications query by ID constantly. The pattern above works well for a single ID, but what about finding a user by ID?

// Repository layer
async function findUserById(id: UserId): Promise<User | null> {
  const row = await pool.query(
    'SELECT * FROM users WHERE id = $1',
    [id as string]  // safe cast: we control the source
  );
  return row.rows[0] ?? null;
}

// Service layer
async function getUserProfile(userId: UserId): Promise<UserProfile> {
  const user = await findUserById(userId);
  if (!user) throw new NotFoundError('User', userId);
  return { id: userId, name: user.name, email: user.email };
}

The pattern stays consistent: the repository layer does the as string cast internally, and the service layer never sees a raw string. If a developer tries to call findUserById(someOrderId), TypeScript stops the build.

Common objections and why they are wrong

“It adds boilerplate.” One type definition and one constructor function per domain concept. That is less boilerplate than the runtime validation and the debugging time you spend hunting mismatched-ID bugs.

“The as unknown as T cast is unsafe.” It is the least unsafe cast in the codebase because it is isolated to a single constructor function per type. Any bug in that cast is caught by the tests for that one function. Compare that to twenty scattered as string casts that each might be wrong in a different way.

“TypeScript has opaque types proposals.” The TC39 proposal for opaque types has been in stage 0 for years. Branded types work today in TypeScript 4.x and later. Do not wait for a standard that may never ship. Use what compiles now.

“Just use classes.” Classes add runtime cost. Every new UserId(s) allocates an object. Every function call that accepts a UserId has to unwrap .value. Branded types are structurally identical to the underlying type at runtime. typeof userId returns "string". JSON.stringify({id: userId}) produces {"id":"abc"}. No custom serialization. No extra allocations. Zero bytes.

“I can just be careful.” No, you cannot. Human attention is the most expensive and unreliable resource in engineering. Let the type system enforce what you cannot trust yourself to remember at 3pm on a Friday.

When to use branded types (and when not to)

Use branded types when:

  • Your function accepts a primitive and the meaning matters (UserId, OrderId, Email)
  • You have two or more domain concepts that share the same underlying type
  • You want to prevent accidental swapping of arguments
  • You want to encode validation guarantees in the type system
  • The type crosses module boundaries (the further it travels, the more value you get)

Do not use branded types when:

  • The type is purely internal to one function and never passed around
  • You already have a class or a dedicated type (like a URL object) that carries the semantics
  • The primitive value has no semantic distinction from other values of the same type (e.g., a measurement in millimeters vs centimeters, which should use a dimensioned type instead)

Testing the boundary constructors

The constructor functions are the most important code to test because they are the single trusted entry point for each type:

describe('UserId', () => {
  it('creates a valid UserId from a hex string', () => {
    const id = UserId.from('507f1f77bcf86cd799439011');
    expect(typeof id).toBe('string');
  });

  it('rejects invalid formats', () => {
    expect(() => UserId.from('not-a-hex')).toThrow('Invalid user ID format');
    expect(() => UserId.from('')).toThrow('Invalid user ID format');
    expect(() => UserId.from('   ')).toThrow('Invalid user ID format');
  });
});

A rejection in the constructor means the error manifests as a clear 400 at the API boundary, not a mysterious 500 deep in a service function that forgot to validate.

Putting it all together: a complete pattern

Here is the full pattern you can drop into your project today:

// brand.ts -- utility
declare const Brand: unique symbol;
export type Brand<T, B> = T & { [Brand]: B };

// brand-factory.ts -- reusable factory
export function createBrandedType<T extends string | number, Name extends string>(
  name: string,
  validate?: (value: T) => boolean
) {
  const from = (value: T): Brand<T, Name> => {
    if (validate && !validate(value)) {
      throw new Error(`Invalid ${name}: ${value}`);
    }
    return value as unknown as Brand<T, Name>;
  };
  return { from };
}

// domains.ts -- your domain types
export const UserId = createBrandedType<string, 'UserId'>('UserId', (s) =>
  /^[a-f0-9]{24}$/i.test(s)
);
export const OrderId = createBrandedType<string, 'OrderId'>('OrderId', (s) =>
  /^ord_[a-z0-9]{20}$/i.test(s)
);
export const SKU = createBrandedType<string, 'SKU'>('SKU', (s) =>
  /^[A-Z]{2,4}-\d{4,8}$/.test(s)
);

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
export type SKU = Brand<string, 'SKU'>;

Now UserId and OrderId are distinct types that cannot be confused at compile time. The factory handles validation and the unsafe cast in one place. Every other file in your codebase is type-safe.

The one-line habit: define a branded type for every string or number ID that crosses a function boundary. Add the factory call, export the type, and use it in your function signatures. TypeScript catches the rest.


A note from Yojji

Type safety at the domain level is one of those investments that feels like overhead on day one and saves your weekend on day ninety. It is the same engineering discipline that Yojji brings to full-cycle product development: build the guardrails into the system so the team can move fast without breaking things. Their senior engineers work across the JavaScript ecosystem (TypeScript, Node.js, React) and cloud-native infrastructure, delivering applications that are both productive to build and safe to run.

Yojji is an international custom software development company founded in 2016, with offices in Europe, the US, and the UK. Their teams cover the full product lifecycle from discovery through DevOps, with a focus on quality, speed, and long-term maintainability.