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

{/* Generated by src/codegen/lint-rule-docs.ts. Do not edit this section. */}

## Configure

Off by default. To turn it on:

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

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

{/* End of the generated section. */}

## Examples

These examples use this config:

```toml bunfig.toml
[lint.rules]
"bun/no-cast-to-brand" = ["error", {
  branded = [{ module = "src/ids", type = "UserId" }],
}]
```

Reported:

{/* bad {"filename": "src/routes.ts", "options": [{"branded": [{"module": "src/ids", "type": "UserId"}]}]} */}
```ts
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:

{/* bad {"filename": "src/routes.ts", "options": [{"branded": [{"module": "src/ids", "type": "UserId"}]}]} */}
```ts
import type { UserId } from "./ids";

const admins = ["u_1", "u_2"] as UserId[];
```

Not reported:

{/* good {"filename": "src/routes.ts", "options": [{"branded": [{"module": "src/ids", "type": "UserId"}]}]} */}
```ts
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:

{/* good {"filename": "src/ids.ts", "options": [{"branded": [{"module": "src/ids", "type": "UserId"}]}]} */}
```ts
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`:

{/* bad {"filename": "src/routes.ts", "options": [{"branded": [{"module": "src/ids", "type": "UserId", "mint": ["unsafeUserId"], "mintedIn": ["src/db.ts"]}]}]} */}
```ts
import { unsafeUserId } from "./ids";

const id = unsafeUserId(request.headers.get("x-user") ?? "");
```

{/* good {"filename": "src/db.ts", "options": [{"branded": [{"module": "src/ids", "type": "UserId", "mint": ["unsafeUserId"], "mintedIn": ["src/db.ts"]}]}]} */}
```ts
import { unsafeUserId } from "./ids";

export const toUser = (row: { id: string }) => ({ id: unsafeUserId(row.id) });
```
