An API handler with a branded `Email` type. Author cast `req.body.email as Email`. TypeScript happy. Runtime: any string accepted, including obvious non-emails.
The vulnerable diff
// types.ts
type Email = string & { __brand: "Email" };
// api/contact.ts
contactRouter.post("/contact", async (req, res) => {
// BUG: as bypasses any runtime check
const email = req.body.email as Email;
await sendWelcome(email); // accepts "not an email"
});What is wrong
Branded (nominal) types in TypeScript are a clever compile-time trick: declare a type that includes an intersection with a unique brand, and the type system treats it as distinct from a plain string. But the brand exists only at compile time — there is nothing at runtime to enforce the brand. `as Email` is a free assertion that any string is an Email. The point of the brand is to force consumers to go through a parser that *does* validate; using `as` defeats the entire pattern.
The attack
Failure path:
req.body.email = "not-an-email"
const email = req.body.email as Email; // accepted by TS
await sendWelcome(email); // tries to send to "not-an-email"
// SMTP error or silent failure.Variants include casting unvalidated path strings as `AbsolutePath`, raw integers as `UserId`, etc.
Mesrai's review comment
mesraipilot · Bot · reviewed 1 min ago
[mesrai] [code-review] [Language] [Type-Hole] [medium]
`as Email` defeats the branded type. The brand exists only at
compile time — the cast lets any string through.
Use Zod's brand for nominal types backed by a runtime parse:
import { z } from "zod";
export const EmailSchema = z.string().email().brand<"Email">();
export type Email = z.infer<typeof EmailSchema>;
// In the handler:
const result = EmailSchema.safeParse(req.body.email);
if (!result.success) {
return res.status(400).json({ error: "invalid email" });
}
await sendWelcome(result.data);
Now `result.data` is typed as Email and the runtime check passed.
Same shape works for AbsolutePath, UserId, PositiveInt, etc.The fix
// api/contact.ts — fixed
import { z } from "zod";
const EmailSchema = z.string().email().brand<"Email">();
type Email = z.infer<typeof EmailSchema>;
contactRouter.post("/contact", async (req, res) => {
const result = EmailSchema.safeParse(req.body.email);
if (!result.success) {
return res.status(400).json({ error: "invalid email" });
}
await sendWelcome(result.data);
});The schema validates the runtime shape AND brands the output. The Email type now has a runtime guarantee behind it. Downstream code receiving `Email` can trust the format.
Why human review missed it
Branded types look like a strong nominal-types pattern in TypeScript but the discipline is fragile if `as` is used freely. Mesrai catches every `as BrandedType` cast and recommends parsing through a validator that produces the brand.
Related rules + further reading
Mesrai rule pack: language/no-brand-as-cast — flags `as` casts to branded types without runtime parse.
Zod docs: Brand types.
Common in fintech / payments codebases where nominal types are heavily used.
Takeaway
Brand requires parsing. Cast does not parse. Use Zod's `.brand()` and the runtime check stays attached. Mesrai catches every bare cast.