TypeScript Nominal Typing and Type Branding
TypeScript is a structurally typed language. This means if two types have the same shape, they are treated as identical, even if they have different names. While structural typing is flexible, it can sometimes lead to silent bugs.
Type Branding is an advanced technique used to achieve nominal typing (differentiating types by name/explicit declaration) inside TypeScript's structural type system.
1. The Problem: Structural Typing Limitations
Consider the following scenario: we have user IDs and post IDs. Both are structurally represented as simple strings in JavaScript.
type UserId = string;
type PostId = string;
function deleteUser(id: UserId) {
console.log(`Deleting user: ${id}`);
}
const postId: PostId = "post_12345";
// This runs without compile errors!
deleteUser(postId); Why is this allowed?
Because UserId and PostId are just aliases for string. Structurally, they are identical, so TypeScript allows you to pass a PostId directly into a function expecting a UserId. This can lead to catastrophic bugs in databases.
2. The Solution: Type Branding
To prevent this, we can "brand" the type by intersecting the primitive type (e.g., string or number) with an object that contains a unique literal signature.
// Define the generic branding helper
type Brand<K, T> = K & { readonly __brand: T };
// Declare branded types
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;Here, UserId is structurally defined as a string that also has a property called __brand containing the string literal "UserId".
Since no normal string contains __brand: "UserId", we have created two distinct types.
3. How to Use Branded Types
To assign a value to a branded type, we must use a Type Assertion (as) because the __brand property doesn't actually exist at runtime. It only exists at compile time for type checks.
// Helper constructors (casting normal strings to branded types)
const makeUserId = (id: string) => id as UserId;
const makePostId = (id: string) => id as PostId;
const myUser = makeUserId("user_01");
const myPost = makePostId("post_99");
function deleteUser(id: UserId) {
console.log(`Deleted user: ${id}`);
}
// Valid call
deleteUser(myUser);
// Compile Error: Type '"PostId"' is not assignable to type '"UserId"'
// deleteUser(myPost); What happens at runtime?
At runtime, the type checks are completely stripped away. myUser and myPost are just normal JavaScript strings. There is zero runtime performance penalty or memory overhead.
4. Where is Branding Useful?
Branding is extremely helpful for:
- Database Identifiers: Differentiating strings like
UserId,PostId,TenantId. - Validated Data: Enforcing that a string has been sanitized or validated. For example, creating a
ValidatedEmailtype that can only be generated by a validation function. - Units of Measurement: Differentiating numbers representing
SecondsvsMilliseconds, orCelsiusvsFahrenheit.
type Seconds = Brand<number, "Seconds">;
type Milliseconds = Brand<number, "Milliseconds">;
function sleep(duration: Milliseconds) {
// ...
}
const delay = 5 as Seconds;
// Compile Error! Prevents unit confusion bugs.
// sleep(delay); Key Takeaways
- TypeScript is structurally typed: types are checked based on their shape/properties, not their names.
- Nominal typing compares types by explicit names or labels, not structure.
- Type Branding is achieved by intersecting a primitive type with a phantom/brand object property (e.g.,
string & { __brand: "Name" }). - Branding is a compile-time construct: it guarantees safety during coding without adding any size or performance overhead to the final production bundle.