Quick Overview
TypeScript compares types by shape, so a `UserId` and a raw `string` are interchangeable to the compiler. A brand adds a phantom field that exists only at compile time, making the two genuinely incompatible.
TypeScript uses a Structural Type System (duck typing). If two types share the same structure, they are considered compatible, even if they have different intent (e.g. UserId and ProductId both being string).
A Branded Type (or Nominal Type) attaches a unique, phantom compile-time "brand" tag to a primitive type to ensure different domain identifiers cannot be accidentally swapped!
// Unique brand tag trick
declare const __brand: unique symbol;
type Brand<T, K extends string> = T & { readonly [__brand]: K };
// Define nominal domain types!
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
// Constructor helper functions (Smart Constructors)
function makeUserId(id: string): UserId {
return id as UserId;
}
function makeProductId(id: string): ProductId {
return id as ProductId;
}
function fetchUserProfile(userId: UserId) {
console.log("Fetching profile for:", userId);
}
const uId = makeUserId("usr_101");
const pId = makeProductId("prod_999");
fetchUserProfile(uId); // Valid!
// fetchUserProfile(pId); // Error: Type 'ProductId' is not assignable to type 'UserId'!
// fetchUserProfile("raw_string"); // Error: Type 'string' is not assignable to 'UserId'!
USD vs EUR, UserId vs OrderId) where mixing primitives causes severe financial or data corruption bugs!Use branded types for domain IDs and monetary units. Next, let me explain Strict Compiler Flags in tsconfig.json!
TypeScript erases entirely at build time, so a `string` annotation produces no runtime check whatsoever. A value arriving from an API is whatever the server actually sent, regardless of what the annotation claims.
`let x = "GET"` infers `string` because the binding can change; `const x = "GET"` infers the literal `"GET"`. That widening rule is why object properties need `as const` to stay narrow.
`string[]` is any number of strings; `[string, number]` is exactly two, in that order. `readonly` removes the mutating methods at compile time, so `push` becomes a type error rather than a runtime surprise.