-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
79: Building a Typed Form Validation Library
I've spent a lot of time reviewing PRs for form-heavy applications, and I keep seeing the same pattern. A developer creates a nice interface for their form data, then writes a validation function that takes any or a Record<string, any>, and finally uses a type assertion like as UserProfile to "force" the result into the correct type. They think they've solved the problem because the TypeScript compiler stops complaining.
The Fallacy of "Casting After Validation"
The problem is that casting is a lie you tell the compiler. If you validate your data and then cast it, you've created a disconnected bridge. If you rename userName to handle in your interface but forget to update the validation logic, TypeScript won't warn you. Your validator will happily check for userName, find it missing, and your casted object will suddenly have an undefined value where you promised the compiler a string would be. This is exactly how "cannot read property of undefined" bugs sneak into production.
// The dangerous way
interface UserProfile {
username: string;
age: number;
}
function validate(data: any) {
if (!data.username) throw new Error("Missing username");
// What if 'age' is missing? The cast below hides the danger.
return data as UserProfile;
}
Letting the Schema Drive the Type
To do this right, we need to flip the relationship. Instead of the interface driving the validator, the validation schema should drive the type. We can achieve this using mapped types. We want to define a set of rules, and then have TypeScript automatically infer the shape of the data those rules are protecting.
Let's build a small library where we define a ValidationRule and a Schema. I prefer using a function that returns a string error or null if the value is valid. It's simple and composable.
type ValidationRule<T> = (value: T) => string | null;
// This is the magic: we map over the keys of T and assign a rule for each value type
type Schema<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
class FormValidator<T> {
constructor(private schema: Schema<T>) {}
validate(data: Partial<T>): { isValid: boolean; errors: Partial<Record<keyof T, string>> } {
const errors: Partial<Record<keyof T, string>> = {};
let isValid = true;
for (const key in this.schema) {
const rule = this.schema[key as keyof T];
const value = data[key as keyof T];
const error = rule(value as T[keyof T]);
if (error) {
errors[key as keyof T] = error;
isValid = false;
}
}
return { isValid, errors };
}
}
Connecting Logic to Constraints
Now, when we actually use this library, the type safety is bidirectional. If I try to add a validation rule for a field that doesn't exist in my UserProfile interface, TypeScript will stop me. If I change the type of age from a number to a string, the rule for age will immediately flag a type error because it's still expecting a number.
I usually keep my rules in a separate utility file so I can reuse them across different forms. Here is how that looks in practice:
interface UserProfile {
username: string;
age: number;
email: string;
}
// Reusable rules
const required = <T>(value: T) => (value === undefined || value === null || value === "") ? "This field is required" : null;
const minLength = (min: number) => (value: string) => value.length < min ? `Must be at least ${min} characters` : null;
const isPositive = (value: number) => value < 0 ? "Must be a positive number" : null;
const userSchema: Schema<UserProfile> = {
username: (val) => required(val) || minLength(3)(val) || null,
age: (val) => required(val) || isPositive(val) || null,
email: (val) => required(val) || (!val.includes("@") ? "Invalid email" : null),
};
const validator = new FormValidator(userSchema);
const result = validator.validate({ username: "Jo", age: -1 });
// result.errors.username will be "Must be at least 3 characters"
// result.errors.age will be "Must be a positive number"
Notice how we aren't using any anywhere in the implementation logic. We're using generics (T) and mapped types to ensure that the FormValidator knows exactly which keys it's dealing with based on the interface provided. This is the "professional" way to handle forms in TypeScript: the types aren't just documentation; they are the guardrails for your logic.
📋 Practical Task
Implement a Typed Product Inventory Validator
You are building a warehouse management system. You need to create a typed validation system for adding new products to the inventory. Complete the following implementation:
- Create an interface
Productwithsku(string),price(number), andquantity(number). - Implement a
ProductSchemausing theSchema<T>pattern from the lesson. - Add a rule for
sku: it must start with "PROD-". - Add a rule for
price: it must be greater than 0. - Add a rule for
quantity: it must be 0 or greater. - Instantiate a
FormValidatorand validate an object with an invalid SKU (e.g., "ITEM-123") and a negative price. - Log the
errorsobject to the console to verify the validation logic caught both mistakes.
There are no comments for now.