Strengthening Input Validation with Branded Types in TypeScript
804 words, 4 minutes read time.
TypeScript offers numerous advantages in building safer, more reliable code, and one advanced but highly effective approach is branded types. If you work with strings in TypeScript, particularly when validating inputs like email addresses or formatted IDs, you’ve likely encountered the challenge of distinguishing specific formats. Branded types bring a way to enforce stronger validation and add clarity to your TypeScript code without sacrificing flexibility. Inspired by Andrew Burgess’s insights in his video Branded Types Give You Stronger Input Validation, this blog will break down the branded type pattern and its applications.
What Are Branded Types?
In TypeScript, branded types allow you to attach a unique identifier to specific string formats, helping differentiate between regular strings and strings that must adhere to particular formats—like email addresses, product IDs, or dates. Although branded types can work with numbers or other primitives, they’re especially useful with strings.
Imagine a function called sendWelcomeEmail
designed to send emails to new users. Normally, we might define the email parameter as a simple string, but this allows any string to be passed, even if it doesn’t resemble a valid email address. Branded types solve this by creating a custom type that isn’t directly assignable from regular strings, requiring validation first.
How to Create a Branded Type in TypeScript
Creating a branded type is relatively simple. Instead of defining a type alias as a basic string, you create a type that’s an intersection of string
and an object with a unique “brand” property. Here’s a sample implementation for an EmailAddress
branded type:
type EmailAddress = string & { __brand: "EmailAddress" };
Here, EmailAddress
is a string with a unique brand that makes it distinct from other strings. The branded type requires validation before assignment, ensuring that any EmailAddress
passed around the system has been verified.
Validating Branded Types with Type Guards
To validate branded types, you’ll need a type guard function to check the format of the input string and “brand” it if it passes. In TypeScript, we define this with a function that returns a type predicate. Below is an example function isEmailAddress
that checks if a string includes @gmail.com
(for simplicity) and returns a branded EmailAddress
type if it does.
function isEmailAddress(email: string): email is EmailAddress { return email.includes("@gmail.com");}
This function validates the email and then “brands” it, letting TypeScript know that we consider this string a genuine EmailAddress
. While this example is basic, you could expand it using regex or other validation rules.
Why Use Branded Types?
Branded types provide robust input validation without the need for constant re-validation. By using branded types at the boundaries of your application—such as at the point where user input enters the system—you can validate and brand the inputs, safely passing them around as trusted values. This results in several key benefits:
- Increased Reliability: Once a value is branded, you know it has passed specific validation criteria.
- Reduced Risk of Bugs: Reduces the chance of invalid data being processed or causing errors down the line.
- Code Readability: When you see
EmailAddress
instead ofstring
, you know instantly what data is being handled. - Enhanced Security: By enforcing validation at the edges of your application, you prevent potentially harmful data from circulating within your system.
Asserting Branded Types
Another way to validate branded types is with assertions. Assertions allow us to throw errors when validation fails, which can be particularly helpful for critical data. Here’s an example of an assertion function for our EmailAddress
type:
function assertEmailAddress(email: string): asserts email is EmailAddress { if (!email.includes("@gmail.com")) { throw new Error("Invalid email address format"); }}
This function throws an error if the email doesn’t match the expected format. Assertions are especially useful when you need immediate error feedback, such as in input validation for API endpoints or user input.
A Practical Use Case: The SignUp
Function
Imagine you’re building a sign-up system where the user provides an email address. Here’s how you might use branded types to ensure that only validated emails are passed to your functions:
function signUp(email: string) { assertEmailAddress(email); sendWelcomeEmail(email); // Now considered a validated EmailAddress}
By calling assertEmailAddress
early, we ensure that sendWelcomeEmail
only receives valid email addresses, adding a layer of safety to our application.
Advantages of TypeScript with Branded Types for Security and Safety
TypeScript’s branded types offer developers the best of both worlds: strong type checking and safe, validated data. With TypeScript’s support for sophisticated type constructs, branded types are especially powerful tools for ensuring that input data meets system expectations.
Using branded types at scale improves both code reliability and security. As noted by Andrew Burgess, “Branded types are a great way to use TypeScript and input validation together to make a stronger, safer system.” This insight underscores the utility of branded types for applications where validated data is essential to system integrity and security.
D. Bryan King
Related Posts
Rate this:
#brandedTypes #codeSafety #dataIntegrity #emailValidation #emailValidationTypeScript #inputValidation #programming #secureCoding #secureInputValidation #strongTyping #typeChecking #TypeScript #TypeScriptAdvancedPatterns #TypeScriptAssertions #TypeScriptBestPractices #TypeScriptBlog #TypeScriptBrandedTypes #typescriptCodingTechniques #TypeScriptDataValidation #TypeScriptEmail #TypeScriptIntersections #TypeScriptPatterns #TypeScriptSecurity #TypeScriptTutorial #TypeScriptTypeGuards #TypeScriptValidation