Skip to content
NewBun 1.4.5 is out. Release notes
AphrodyBun

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:

bunfig.toml
[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.

bunfig.toml
[lint.rules]
"bun/no-cast-to-brand" = ["error", {
  # branded = <list of objects>,
}]

Examples

These examples use this config:

bunfig.toml
[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:

KeyDefaultWhat it does
modulerequiredThe end of the path to the file that declares the type, without the extension, like "src/ids" or "ids". A package name works too.
typerequiredThe 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) });