TypeScript is a Structural Type System, which means the TypeScript Compiler recognizes two objects as the same type if they have the same properties. In some scenarios, you might not want the behavior of Structural Typing.
type Car = { id: number; name: string };
type Bike = { id: number; name: string };
const bike: Bike = await prisma.bike.findUnique(
{ where: { id: 1 } }
); // { id: 1, name: "XL250R" };
function deleteCar(car: Car) {
console.log(car);
}
// You accidentally deleted a car with id 1.
deleteCar(bike);
Branded types can prevent these scenarios by attaching a unique key to an object. Let's look at an example.
// Declare `__brand` variable that will be removed at runtime.
declare const __brand: unique symbol;
// `Brand` is a utility type that makes T a branded type having U (unique symbol)
type Brand<T, U> = T & { readonly [__brand]: U };
// Now `Car` and `Bike` are different types.
type Car = { id: Brand<number, "Car">; name: string };
type Bike = { id: Brand<number, "Bike">; name: string };
const car: Car = {
id: 1 as Brand<number, "Car">,
name: "JIMNY",
};
deleteCar(bike); // Error happens.
deleteCar(car); // Success to save!
It's a good idea to use Brand types with a class constructor when creating a data model such as Car
or Bike
.
class Car {
public id: Brand<number, "Car">;
public name: string;
constructor(id: number, name: string) {
this.id = id as Brand<number, "Car">;
this.name = name;
}
}
const car = new Car(1, "JIMNY");
If you use Zod, you can use the .brand method in a Zod schema.
import { BRAND, z } from "zod";
class Car {
public id: number & BRAND<"Car">;
public name: string;
constructor(id: number, name: string) {
this.id = z.number().brand<"Car">().parse(id);
this.name = name;
}
}
In conclusion, using Brand types can help you avoid type mismatches by ensuring that objects are identified by their types. This approach is particularly useful when working with data models that require strict type safety.