Careeroza
Branded Types
Telling Same-shaped Types Apart
TypeScript is structural: UserId and OrderId declared as string are interchangeable, so passing one for the other is not caught. A brand adds a fake property that exists only in the type system, making them distinct (nominal typing).
Example
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Paise = Brand<number, "Paise">;
const UserId = (s: string) => s as UserId;
const OrderId = (s: string) => s as OrderId;
function toPaise(rupees: number): Paise {
if (!Number.isFinite(rupees) || rupees < 0) throw new Error("Invalid amount");
return Math.round(rupees * 100) as Paise;
}
function refund(orderId: OrderId, amount: Paise): string {
return `Refund ${amount} paise for ${orderId}`;
}
const order = OrderId("OD-1001");
const user = UserId("U-77");
console.log(refund(order, toPaise(499.5)));
console.log(user.toUpperCase()); // still a string at runtimeOutput
Refund 49950 paise for OD-1001 U-77
Mixing Up IDs
Example
Passing a UserId as an OrderId, or a plain number as Paise, is now an error (TS2345).
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Paise = Brand<number, "Paise">;
declare function refund(orderId: OrderId, amount: Paise): string;
declare const userId: UserId;
refund(userId, 49950);Compiler output (tsc)
Line 10: error TS2345: Argument of type 'UserId' is not assignable to parameter of type 'OrderId'.
Type 'UserId' is not assignable to type '{ readonly [brand]: "OrderId"; }'.
Types of property '[brand]' are incompatible.
Type '"UserId"' is not assignable to type '"OrderId"'.Tip: Brand money values (paise vs rupees), IDs, validated emails and sanitized HTML. Create branded values only in one validating function.