TypeScript Type Branding (ts-brand)
ts-brand is a TypeScript library that enables nominal typing through 'type branding,' a technique that intersects a base type with an object type containing a non-existent property. This allows developers to create distinct types (e.g., `PostId`, `UserId`) from a common primitive (like `number`), preventing accidental assignment bugs at compile time even though they share the same runtime representation. The library is currently at version 0.2.0, indicating it's an early-stage but active project. It ships with TypeScript types inherently, as its functionality is entirely type-system based. A key differentiator is its emphasis on ensuring brand uniqueness, offering methods like using recursive interface types as branding to prevent accidental conflation of brands across different definitions, which is a common pitfall in simpler branding implementations. The project's release cadence appears to be driven by contributions, without a fixed schedule.
Common errors
-
Type 'number' is not assignable to type 'Brand<number, "User">'.
cause Attempting to assign a raw `number` (or other base type) directly to a branded type without an explicit type assertion.fixUse a type assertion (`as Brand<number, 'User'>`) to explicitly tell TypeScript that the value conforms to the branded type after validation, or use a utility function that performs the assertion and returns the branded type. `const myUserId: User['id'] = rawNumber as User['id'];` -
Type 'Brand<number, Post>' is not assignable to type 'Brand<number, User>'.
cause Attempting to assign a value of one branded type to a variable expecting a different branded type, even if their underlying base types are the same.fixThis is the intended behavior of nominal typing. If you genuinely need to convert between distinct branded types, explicitly 'unbrand' to the base type and then 'rebrand' to the target type, typically with a type assertion and appropriate validation logic. E.g., `const convertedUserId: User['id'] = (postAuthorId as number) as User['id'];` -
Cannot find name 'Brand'.
cause This typically occurs when trying to use `Brand` in a JavaScript file that doesn't import types, or when an incorrect CommonJS `require` syntax is used for a TypeScript type-only module.fixEnsure you are using `import { Brand } from 'ts-brand';` within a TypeScript file (`.ts` or `.tsx`). If working in a mixed JavaScript/TypeScript project, remember that types are a compile-time construct and not available at runtime via `require`.
Warnings
- gotcha Branded types in `ts-brand` are a compile-time construct and have no runtime impact. At runtime, a branded type like `Brand<number, 'user'>` will simply be a `number`. Any runtime checks or conversions must be implemented manually.
- gotcha Two branded types are considered the same if they share the same base type and branding type. Care must be taken to ensure that the `Branding` type parameter is truly unique to avoid unintended type conflation across different parts of a codebase.
- gotcha When upgrading TypeScript, new strictness checks or changes in type inference might expose previously unflagged issues related to type branding or assertions.
- gotcha Since `ts-brand` is at an early version (0.2.0), its API surface might evolve, and future minor or major releases could introduce breaking changes or significant modifications to existing types.
Install
-
npm install ts-brand -
yarn add ts-brand -
pnpm add ts-brand
Imports
- Brand
const { Brand } = require('ts-brand');import { Brand } from 'ts-brand';
Quickstart
import { Brand } from 'ts-brand';
// Define the base API
declare function getPost(postId: Post['id']): Promise<Post>;
declare function getUser(userId: User['id']): Promise<User>;
interface User {
id: Brand<number, User>; // Branding 'number' with the User interface for strong uniqueness
name: string;
}
interface Post {
id: Brand<number, Post>; // Branding 'number' with the Post interface
authorId: User['id']; // Correctly typed authorId as a User's branded ID
title: string;
body: string;
}
// A function that correctly retrieves the author of a post
function authorOfPost(postId: Post['id']): Promise<User> {
return getPost(postId).then(post => getUser(post.authorId));
}
// Example of how a type error is caught (uncomment to see error)
/*
function buggyAuthorOfPost(postId: Post['id']): Promise<User> {
// This would correctly fail to compile because post.id is Post['id']
// and getUser expects User['id'], even though both are based on 'number'.
return getPost(postId).then(post => getUser(post.id));
}
*/
// To use branded types, you typically cast a base type value.
const postIdValue: number = 123;
const userIdValue: number = 456;
const myPostId: Post['id'] = postIdValue as Post['id'];
const myUserId: User['id'] = userIdValue as User['id'];
// This would compile:
authorOfPost(myPostId);
// This would be a compile-time error:
// getUser(myPostId); // Type 'Brand<number, Post>' is not assignable to type 'Brand<number, User>'.