Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on input validation using TypeScriptโs powerful type system! ๐ In this guide, weโll explore how to leverage TypeScriptโs types to create robust validation that catches errors before they reach your users.
Youโll discover how type-based validation can transform your applicationโs reliability. Whether youโre building APIs ๐, processing forms ๐, or handling user data ๐ค, understanding type-based validation is essential for writing secure, maintainable code.
By the end of this tutorial, youโll feel confident implementing rock-solid validation in your TypeScript projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Type-Based Validation
๐ค What is Type-Based Validation?
Type-based validation is like having a security guard ๐ฎโโ๏ธ at the entrance of your application. Think of it as a bouncer at a club who checks IDs - but instead of checking ages, itโs checking if your data has the right shape and values!
In TypeScript terms, type-based validation uses the type system to ensure data conforms to expected structures at compile-time AND runtime. This means you can:
- โจ Catch invalid data before it causes problems
- ๐ Get compile-time safety for your validation logic
- ๐ก๏ธ Create reusable validation patterns
- ๐ Self-documenting validation rules
๐ก Why Use Type-Based Validation?
Hereโs why developers love type-based validation:
- Type Safety ๐: Validation rules are enforced by TypeScript
- Better Developer Experience ๐ป: Autocomplete for valid values
- Runtime Protection ๐ก๏ธ: Catch bad data from external sources
- Maintainability ๐ง: Change validation in one place
Real-world example: Imagine building a user registration system ๐. With type-based validation, you can ensure emails are valid, passwords meet requirements, and ages are reasonable - all with type-safe guarantees!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Type-Based Validation!
type Email = string & { __brand: "Email" };
type PositiveNumber = number & { __brand: "PositiveNumber" };
// ๐จ Creating validation functions
const isEmail = (value: string): value is Email => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
};
const isPositiveNumber = (value: number): value is PositiveNumber => {
return value > 0;
};
// ๐ Type-safe validation
const validateEmail = (input: string): Email => {
if (!isEmail(input)) {
throw new Error("Invalid email! ๐ง");
}
return input;
};
๐ก Explanation: We use branded types (nominal types) to create distinct types for validated data. The is
keyword creates type predicates that TypeScript understands!
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: User input validation
interface UserInput {
name: string;
email: string;
age: number;
}
interface ValidatedUser {
name: string;
email: Email;
age: PositiveNumber;
}
// ๐จ Pattern 2: Validation result types
type ValidationResult<T> =
| { success: true; data: T }
| { success: false; errors: string[] };
// ๐ Pattern 3: Validation pipeline
const validateUser = (input: UserInput): ValidationResult<ValidatedUser> => {
const errors: string[] = [];
// ๐ Validate each field
if (input.name.length < 2) {
errors.push("Name too short! ๐");
}
if (!isEmail(input.email)) {
errors.push("Invalid email! ๐ง");
}
if (!isPositiveNumber(input.age) || input.age > 120) {
errors.push("Invalid age! ๐");
}
if (errors.length > 0) {
return { success: false, errors };
}
return {
success: true,
data: {
name: input.name,
email: input.email as Email,
age: input.age as PositiveNumber
}
};
};
๐ก Practical Examples
๐ Example 1: E-Commerce Order Validation
Letโs build something real:
// ๐๏ธ Define our validated types
type ProductId = string & { __brand: "ProductId" };
type Quantity = number & { __brand: "Quantity" };
type Price = number & { __brand: "Price" };
// ๐ Order validation system
interface OrderItem {
productId: string;
quantity: number;
price: number;
}
interface ValidatedOrderItem {
productId: ProductId;
quantity: Quantity;
price: Price;
}
class OrderValidator {
// ๐ฏ Validate product ID format
private isValidProductId(id: string): id is ProductId {
return /^PROD-[0-9]{6}$/.test(id);
}
// ๐ฆ Validate quantity
private isValidQuantity(qty: number): qty is Quantity {
return Number.isInteger(qty) && qty > 0 && qty <= 100;
}
// ๐ฐ Validate price
private isValidPrice(price: number): price is Price {
return price > 0 && price <= 10000 &&
Number(price.toFixed(2)) === price;
}
// โ
Validate entire order item
validateOrderItem(item: OrderItem): ValidationResult<ValidatedOrderItem> {
const errors: string[] = [];
if (!this.isValidProductId(item.productId)) {
errors.push(`Invalid product ID: ${item.productId} ๐ซ`);
}
if (!this.isValidQuantity(item.quantity)) {
errors.push(`Invalid quantity: ${item.quantity} ๐ฆ`);
}
if (!this.isValidPrice(item.price)) {
errors.push(`Invalid price: $${item.price} ๐ธ`);
}
if (errors.length > 0) {
return { success: false, errors };
}
return {
success: true,
data: {
productId: item.productId as ProductId,
quantity: item.quantity as Quantity,
price: item.price as Price
}
};
}
}
// ๐ฎ Let's use it!
const validator = new OrderValidator();
const result = validator.validateOrderItem({
productId: "PROD-123456",
quantity: 5,
price: 29.99
});
if (result.success) {
console.log("โ
Order validated!", result.data);
} else {
console.log("โ Validation failed:", result.errors);
}
๐ฏ Try it yourself: Add validation for discount codes and shipping addresses!
๐ฎ Example 2: Game Character Creation
Letโs make it fun:
// ๐ Character validation system
type CharacterName = string & { __brand: "CharacterName" };
type Level = number & { __brand: "Level" };
type SkillPoints = number & { __brand: "SkillPoints" };
interface CharacterInput {
name: string;
class: string;
level: number;
skills: {
strength: number;
agility: number;
intelligence: number;
};
}
class CharacterValidator {
// ๐ฎ Valid character classes
private readonly validClasses = ["warrior", "mage", "rogue", "healer"] as const;
type CharacterClass = typeof this.validClasses[number];
// ๐ Name validation
private isValidName(name: string): name is CharacterName {
return name.length >= 3 &&
name.length <= 20 &&
/^[a-zA-Z0-9_]+$/.test(name);
}
// ๐ฏ Level validation
private isValidLevel(level: number): level is Level {
return Number.isInteger(level) && level >= 1 && level <= 100;
}
// ๐ช Skill points validation
private isValidSkillPoints(points: number): points is SkillPoints {
return Number.isInteger(points) && points >= 0 && points <= 100;
}
// ๐๏ธ Validate character creation
validateCharacter(input: CharacterInput) {
const errors: string[] = [];
// ๐จ Validate name
if (!this.isValidName(input.name)) {
errors.push("Character name must be 3-20 alphanumeric characters! ๐");
}
// ๐ก๏ธ Validate class
if (!this.validClasses.includes(input.class as any)) {
errors.push(`Invalid class! Choose: ${this.validClasses.join(", ")} โ๏ธ`);
}
// ๐ Validate level
if (!this.isValidLevel(input.level)) {
errors.push("Level must be between 1-100! ๐ฏ");
}
// ๐ช Validate skills
const totalSkills = Object.values(input.skills).reduce((a, b) => a + b, 0);
const maxSkillPoints = input.level * 5; // 5 points per level
if (totalSkills > maxSkillPoints) {
errors.push(`Too many skill points! Max: ${maxSkillPoints} ๐ซ`);
}
Object.entries(input.skills).forEach(([skill, points]) => {
if (!this.isValidSkillPoints(points)) {
errors.push(`Invalid ${skill} points! ๐`);
}
});
return errors.length === 0
? { success: true as const, character: input }
: { success: false as const, errors };
}
}
// ๐ฎ Create a character!
const characterValidator = new CharacterValidator();
const hero = characterValidator.validateCharacter({
name: "DragonSlayer42",
class: "warrior",
level: 10,
skills: { strength: 30, agility: 10, intelligence: 10 }
});
if (hero.success) {
console.log("๐ Character created!", hero.character);
} else {
console.log("โ Invalid character:", hero.errors);
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Generic Validation Functions
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced generic validator
type Validator<T> = (value: unknown) => value is T;
// ๐ช Combine validators
const combine = <T>(...validators: Validator<any>[]): Validator<T> => {
return (value: unknown): value is T => {
return validators.every(validator => validator(value));
};
};
// โจ Create reusable validators
const minLength = (min: number): Validator<string> => {
return (value: unknown): value is string => {
return typeof value === "string" && value.length >= min;
};
};
const maxLength = (max: number): Validator<string> => {
return (value: unknown): value is string => {
return typeof value === "string" && value.length <= max;
};
};
const matches = (pattern: RegExp): Validator<string> => {
return (value: unknown): value is string => {
return typeof value === "string" && pattern.test(value);
};
};
// ๐๏ธ Compose complex validators
type Username = string & { __brand: "Username" };
const isUsername = combine<Username>(
minLength(3),
maxLength(20),
matches(/^[a-zA-Z0-9_]+$/)
);
๐๏ธ Advanced Topic 2: Schema-Based Validation
For the brave developers:
// ๐ Type-safe schema validation
type Schema<T> = {
[K in keyof T]: Validator<T[K]>;
};
const validateSchema = <T>(
schema: Schema<T>,
input: unknown
): ValidationResult<T> => {
if (typeof input !== "object" || input === null) {
return { success: false, errors: ["Input must be an object! ๐ฆ"] };
}
const errors: string[] = [];
const result = {} as T;
for (const [key, validator] of Object.entries(schema)) {
const value = (input as any)[key];
if (!validator(value)) {
errors.push(`Invalid ${key}! ๐ซ`);
} else {
(result as any)[key] = value;
}
}
return errors.length === 0
? { success: true, data: result }
: { success: false, errors };
};
// ๐จ Use the schema validator
const userSchema: Schema<ValidatedUser> = {
name: (v): v is string => typeof v === "string" && v.length > 0,
email: isEmail,
age: isPositiveNumber
};
const validated = validateSchema(userSchema, {
name: "Alice",
email: "[email protected]",
age: 25
});
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Trusting External Data
// โ Wrong way - trusting API data!
interface ApiResponse {
user: ValidatedUser; // ๐ฐ Assuming it's already validated!
}
const handleApiResponse = (response: ApiResponse) => {
console.log(response.user.email); // ๐ฅ Might not be valid!
};
// โ
Correct way - always validate external data!
const handleApiResponse = (response: unknown) => {
const result = validateUser(response as UserInput);
if (result.success) {
console.log("Valid user! โ
", result.data.email);
} else {
console.log("Invalid data! โ ๏ธ", result.errors);
}
};
๐คฏ Pitfall 2: Forgetting Edge Cases
// โ Dangerous - missing edge cases!
const isPositive = (n: number): n is PositiveNumber => {
return n > 0; // ๐ฅ What about NaN, Infinity?
};
// โ
Safe - handle all cases!
const isPositive = (n: number): n is PositiveNumber => {
return Number.isFinite(n) && n > 0;
};
// โ Incomplete email validation
const badEmailCheck = (email: string) => {
return email.includes("@"); // ๐ฑ Too simple!
};
// โ
Proper email validation
const goodEmailCheck = (email: string): email is Email => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email) && email.length <= 254; // RFC limit! ๐
};
๐ ๏ธ Best Practices
- ๐ฏ Validate at Boundaries: Always validate data entering your system
- ๐ Clear Error Messages: Help users fix validation errors
- ๐ก๏ธ Defense in Depth: Multiple validation layers for critical data
- ๐จ Reusable Validators: Build a library of common validators
- โจ Type-Safe Results: Use discriminated unions for validation results
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Form Validation System
Create a type-safe form validation system:
๐ Requirements:
- โ Support multiple field types (text, email, number, date)
- ๐ท๏ธ Custom validation rules per field
- ๐ค Real-time validation feedback
- ๐ Complex validations (date ranges, password strength)
- ๐จ Reusable validation components
๐ Bonus Points:
- Add async validation (checking username availability)
- Implement cross-field validation (password confirmation)
- Create a validation rule builder
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe form validation system!
type FieldValidator<T> = {
validate: (value: T) => ValidationResult<T>;
message: string;
};
type FormSchema<T> = {
[K in keyof T]: FieldValidator<T[K]>[];
};
class FormValidator<T> {
constructor(private schema: FormSchema<T>) {}
// ๐ Validate single field
validateField<K extends keyof T>(
field: K,
value: T[K]
): string[] {
const validators = this.schema[field];
const errors: string[] = [];
for (const validator of validators) {
const result = validator.validate(value);
if (!result.success) {
errors.push(validator.message);
}
}
return errors;
}
// โ
Validate entire form
validateForm(data: T): {
isValid: boolean;
errors: Partial<Record<keyof T, string[]>>;
} {
const errors: Partial<Record<keyof T, string[]>> = {};
let isValid = true;
for (const field in this.schema) {
const fieldErrors = this.validateField(field, data[field]);
if (fieldErrors.length > 0) {
errors[field] = fieldErrors;
isValid = false;
}
}
return { isValid, errors };
}
}
// ๐๏ธ Common validators
const required = <T>(): FieldValidator<T> => ({
validate: (value) => ({
success: value !== null && value !== undefined && value !== "",
errors: []
}),
message: "This field is required! ๐"
});
const email = (): FieldValidator<string> => ({
validate: (value) => ({
success: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
errors: []
}),
message: "Please enter a valid email! ๐ง"
});
const minLength = (min: number): FieldValidator<string> => ({
validate: (value) => ({
success: value.length >= min,
errors: []
}),
message: `Must be at least ${min} characters! ๐`
});
const passwordStrength = (): FieldValidator<string> => ({
validate: (value) => {
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[!@#$%^&*]/.test(value);
const isLongEnough = value.length >= 8;
return {
success: hasUpper && hasLower && hasNumber && hasSpecial && isLongEnough,
errors: []
};
},
message: "Password must be 8+ chars with uppercase, lowercase, number, and special char! ๐"
});
// ๐ฎ Test it out!
interface SignupForm {
username: string;
email: string;
password: string;
age: number;
}
const signupValidator = new FormValidator<SignupForm>({
username: [required(), minLength(3)],
email: [required(), email()],
password: [required(), passwordStrength()],
age: [
required(),
{
validate: (age) => ({ success: age >= 18 && age <= 120, errors: [] }),
message: "Must be between 18-120! ๐"
}
]
});
// ๐ Validate a form
const formData: SignupForm = {
username: "coder123",
email: "[email protected]",
password: "Secure123!",
age: 25
};
const result = signupValidator.validateForm(formData);
console.log(result.isValid ? "โ
Form is valid!" : "โ Form has errors:", result.errors);
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create type-safe validators with confidence ๐ช
- โ Avoid common validation mistakes that trip up beginners ๐ก๏ธ
- โ Apply validation patterns in real projects ๐ฏ
- โ Debug validation issues like a pro ๐
- โ Build secure applications with TypeScript! ๐
Remember: Validation is your first line of defense against bad data. TypeScript makes it type-safe AND powerful! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered type-based validation!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a validation library for your projects
- ๐ Move on to our next tutorial: Authentication and Authorization with Types
- ๐ Share your validation patterns with the community!
Remember: Every secure application starts with proper validation. Keep coding, keep validating, and most importantly, have fun! ๐
Happy coding! ๐๐โจ