一键导入
branded-types
Use when primitive types need semantic distinction. Use when string or number types have different meanings. Use when you need nominal typing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when primitive types need semantic distinction. Use when string or number types have different meanings. Use when you need nominal typing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when defining global types. Use when augmenting window. Use when typing environment variables. Use when working with build-time constants. Use when configuring type definitions.
Use when migrating JavaScript to TypeScript. Use when gradually adopting TypeScript. Use when working with mixed codebases. Use when converting large projects. Use when teams are learning TypeScript.
Use when writing asynchronous code. Use when tempted to use callbacks. Use when composing multiple async operations.
Use when creating types from example data. Use when types don't match all cases. Use when API responses vary.
Use when writing type annotations on variables. Use when TypeScript can infer the type. Use when code feels cluttered with types.
Use when defining array-like types. Use when tempted to use number as index type. Use when understanding array keys.
| name | branded-types |
| description | Use when primitive types need semantic distinction. Use when string or number types have different meanings. Use when you need nominal typing. |
Add phantom types to distinguish semantically different values.
TypeScript uses structural typing, but sometimes you need nominal typing - values that are distinct because you SAY they are, not because they have different shapes. Brands let you do this without runtime overhead.
ALWAYS use brands when primitive types have different semantic meanings.
Remember:
When different primitives can be confused:
// ❌ Any string can be passed
function readFile(path: string) { ... }
readFile('foo.txt'); // Relative path - might fail
readFile('/home/foo.txt'); // Absolute path - works
// TypeScript can't tell the difference!
type AbsolutePath = string & { _brand: 'abs' };
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startsWith('/');
}
function listAbsolutePath(path: AbsolutePath) {
// Can only be called with validated paths
}
function f(path: string) {
// Must check before using
if (isAbsolutePath(path)) {
listAbsolutePath(path); // OK: path is now AbsolutePath
}
listAbsolutePath(path);
// ~~~~ Error: string not assignable to AbsolutePath
}
You can't actually create a value that is both a string and has a _brand property:
type AbsolutePath = string & { _brand: 'abs' };
// This intersection is "impossible" at runtime
// But TypeScript still uses it for type checking
The only way to get an AbsolutePath is to:
type Meters = number & { _brand: 'meters' };
type Seconds = number & { _brand: 'seconds' };
type MetersPerSecond = number & { _brand: 'm/s' };
const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;
function calculateSpeed(distance: Meters, time: Seconds): MetersPerSecond {
return (distance / time) as MetersPerSecond;
}
const d = meters(100);
const t = seconds(10);
const speed = calculateSpeed(d, t); // OK
calculateSpeed(100, 10); // Error: number not assignable to Meters
calculateSpeed(t, d); // Error: can't swap distance and time!
Caveat: Arithmetic operations lose the brand:
const doubled = d * 2;
// ^? const doubled: number (brand lost)
type SanitizedHTML = string & { _brand: 'sanitized' };
type UserId = string & { _brand: 'userId' };
type Email = string & { _brand: 'email' };
function sanitize(html: string): SanitizedHTML {
// Actually sanitize the HTML
return html.replace(/<script>/g, '') as SanitizedHTML;
}
function setInnerHTML(el: Element, html: SanitizedHTML) {
el.innerHTML = html; // Safe: we know it's sanitized
}
// Can't pass unsanitized strings
setInnerHTML(el, '<script>alert("xss")</script>');
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Error!
// Must sanitize first
setInnerHTML(el, sanitize(userInput)); // OK
type UserId = string & { _brand: 'userId' };
type PostId = string & { _brand: 'postId' };
function getUser(id: UserId): User { ... }
function getPost(id: PostId): Post { ... }
declare const userId: UserId;
declare const postId: PostId;
getUser(userId); // OK
getUser(postId); // Error: PostId not assignable to UserId
type SortedList<T> = T[] & { _brand: 'sorted' };
function isSorted<T>(xs: T[]): xs is SortedList<T> {
for (let i = 0; i < xs.length - 1; i++) {
if (xs[i] > xs[i + 1]) return false;
}
return true;
}
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
// Can assume xs is sorted
let low = 0, high = xs.length - 1;
while (high >= low) {
const mid = low + Math.floor((high - low) / 2);
const v = xs[mid];
if (v === x) return true;
[low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
}
return false;
}
const nums = [1, 3, 5, 7, 9];
if (isSorted(nums)) {
binarySearch(nums, 5); // OK: nums is SortedList<number>
}
declare const brand: unique symbol;
type Meters = number & { [brand]: 'meters' };
// Can't be faked because brand isn't exported
class ValidatedEmail {
private readonly _brand!: 'email';
constructor(public readonly value: string) {
if (!value.includes('@')) throw new Error('Invalid email');
}
}
interface Vector2D {
x: number;
y: number;
z?: never; // Explicitly prevent z
}
function norm(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
const v3d = { x: 3, y: 4, z: 5 };
norm(v3d); // Error: z is incompatible with never
Or use brands:
type Vector2D = { x: number; y: number } & { _brand: '2d' };
type Vector3D = { x: number; y: number; z: number } & { _brand: '3d' };
Pressure: "Type alias is simpler: type UserId = string"
Response: Type aliases don't prevent mixing up different string types.
Action: Use brands when semantic distinction matters.
Pressure: "Adding properties to primitives costs memory"
Response: Brands are phantom types - they don't exist at runtime.
Action: Use brands freely; there's no runtime cost.
| Excuse | Reality |
|---|---|
| "We'll be careful" | Mistakes happen, especially in large codebases |
| "Type alias is enough" | Aliases don't prevent cross-assignment |
| "Too much ceremony" | Prevents bugs that are hard to track down |
// Basic brand pattern
type Brand<T, B extends string> = T & { _brand: B };
type UserId = Brand<string, 'userId'>;
type Meters = Brand<number, 'meters'>;
// Type guard pattern
function isX(val: T): val is BrandedT { ... }
// Factory pattern
const meters = (n: number) => n as Meters;
Use brands to give semantic meaning to primitives.
Brands add no runtime overhead but prevent mixing up values that happen to have the same underlying type. Use them for IDs, paths, units, validated strings, and any primitive where semantic distinction matters.
Based on "Effective TypeScript" by Dan Vanderkam, Item 64: Consider Brands for Nominal Typing.