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> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
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")));