TypeScript: branded types for validated and escaped data

Many vulnerabilities come from using raw input where checked input was expected: an unescaped string put into HTML, or an unvalidated string used as an email address. A branded type tags a base type, here string, with a phantom property so that the compiler treats Email and SafeHtml as different from plain string.

TypeScript
type Brand<T, B extends string> = T & { readonly __brand: B };
type Email = Brand<string, "Email">;
type SafeHtml = Brand<string, "SafeHtml">;

function parseEmail(text: string): Email | null {
  return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(text) ? (text as Email) : null;
}

function escapeHtml(text: string): SafeHtml {
  const map: Record<string, string> = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
  return text.replace(/[&<>"']/g, (ch) => map[ch]) as SafeHtml;
}

function sendTo(address: Email): string {
  return "sending to " + address;
}

function paragraph(html: SafeHtml): string {
  return "<p>" + html + "</p>";
}

for (const input of ["ada@example.com", "not-an-email"]) {
  const email = parseEmail(input);
  console.log(email === null ? "rejected: " + input : sendTo(email));
}
console.log(paragraph(escapeHtml("<b>bold</b> & more")));
Read the full lesson