Given a Branded / Nominal interface, they look about the same usage wise but the branded type disappears at runtime (whether that is a benefit or a detriment depends on your use case):
type Email = Branded<string, 'email'>
function Email(maybeEmail: string): Email {
assertIsEmail(maybeEmail);
return maybeEmail;
}
function assertIsEmail(value: string): asserts value is Email {
if(!isEmail(value)) throw TypeError("Not an email");
return value;
}
function isEmail(value: string): value is Email {
return value.contains("@");
}
Versus:
class Email {
#email: string;
public constructor(maybeEmail: string) {
if(!isEmail(maybeEmail)) throw TypeError("Not an email");
this.#email = maybeEmail;
}
valueOf() {
return this.#email;
}
}