Linter · Rules
bun/no-cast-to-brand
Disallow casts to a branded type outside the module that declares it
A branded type such as UserId says "this string has been checked". That's only true if every UserId comes from the function that does the check. Writing raw as UserId anywhere else creates one from an unchecked value, and the compiler accepts it.
The rule does nothing until you list your branded types in branded.
Configure
Off by default. To turn it on:
[lint.rules]
"bun/no-cast-to-brand" = "error" # or "warn"
Every option, set to its default. The ones that are commented out are unset by default.
[lint.rules]
"bun/no-cast-to-brand" = ["error", {
# branded = <list of objects>,
}]
Examples
These examples use this config:
[lint.rules]
"bun/no-cast-to-brand" = ["error", {
branded = [{ module = "src/ids", type = "UserId" }],
}]
Reported:
import type { UserId } from "./ids";
export function userOf(params: { id: string }) {
return load(params.id as UserId);
}
Casts to an array, tuple, or union of the type, or to an alias of it, are reported too:
import type { UserId } from "./ids";
const admins = ["u_1", "u_2"] as UserId[];
Not reported:
import { parseUserId } from "./ids";
export function userOf(params: { id: string }) {
return load(parseUserId(params.id));
}
The module that declares the type is where the cast belongs:
export type UserId = string & { readonly brand: unique symbol };
export function parseUserId(text: string): UserId {
if (!text.startsWith("u_")) throw new TypeError(`not a user ID: ${text}`);
return text as UserId;
}
Options
branded is a list of objects with these keys:
| Key | Default | What it does |
|---|---|---|
module | required | The end of the path to the file that declares the type, without the extension, like "src/ids" or "ids". A package name works too. |
type | required | The type's name as the module exports it. |
mint | [] | Functions in the module that create a value of the type without checking it. |
mintedIn | [] | The ends of the paths to the files that can import the mint functions, with the extension. |
With mint, importing or re-exporting one of those functions is reported in every file that isn't in mintedIn:
import { unsafeUserId } from "./ids";
const id = unsafeUserId(request.headers.get("x-user") ?? "");
import { unsafeUserId } from "./ids";
export const toUser = (row: { id: string }) => ({ id: unsafeUserId(row.id) });