Published on January 12, 2026
TypeScript compiles to JavaScript, and all type information is erased. When data arrives from an API, file upload, or user input, TypeScript cannot verify it matches your types. You might declare that an API returns User type, but at runtime you get whatever the server sends. If the server changes its response format or returns malformed data, your TypeScript code will not catch it until something breaks.
This gap between compile-time types and runtime reality is dangerous. Your code assumes data has certain properties, but those assumptions can be violated at runtime. Accessing user.email might throw an error if the property does not exist, even though TypeScript says it should be there.
Runtime validation bridges this gap. It checks that data actually matches your types before using it. This catches bugs early and provides clear error messages instead of cryptic undefined errors deep in your code.
Popular validation libraries include Zod, Yup, io-ts, and AJV. Each has different ergonomics and performance characteristics. Zod has gained popularity for its TypeScript-first design and excellent type inference.
Validation should happen at system boundaries: API responses, file uploads, environment variables, configuration files, database queries. Internal functions can trust their inputs if boundaries are validated. This layered approach balances safety with performance.
Type erasure is fundamental to TypeScript architecture. TypeScript is a development tool, not a runtime. This keeps JavaScript output fast but requires explicit runtime checks.
Any type bypasses compile-time checking. Avoid any whenever possible. Use unknown for truly unknown data, then validate before use. Unknown forces validation.
Third-party APIs are untrusted by definition. Backend changes, network corruption, or malicious responses can break assumptions. Always validate external data regardless of documentation.
Define schemas using Zod syntax that mirrors TypeScript types. Zod schemas are runtime objects that perform validation and automatically infer TypeScript types. This keeps your types and validation logic in sync.
Parse untrusted data with schema.parse() which throws on invalid data, or schema.safeParse() which returns a result object. SafeParse is better for handling validation errors gracefully without try-catch blocks.
Compose schemas for complex types. Define base schemas for primitives, then build up object schemas. Zod supports unions, intersections, arrays, records, and recursive types. This mirrors TypeScript type construction.
Transform data during validation using .transform(). This is useful for parsing dates, normalizing strings, or converting formats. Transformations happen after validation passes, so you can safely assume the data structure is correct.
Refinements add custom validation logic beyond type checking. Use .refine() to enforce business rules like "email must be from company domain" or "password must contain special characters." Refinements can access the entire object, enabling cross-field validation.
Default values and optional fields reduce boilerplate. Mark fields optional with .optional() or provide defaults with .default(). This handles missing data gracefully instead of failing validation.
Error messages should be customized for user-facing validation. Zod allows setting custom error messages per field. Generic "invalid_type" errors confuse users. "Email address is required" is clear.
Performance matters for high-traffic APIs. Zod is fast enough for most use cases, but parsing large payloads can add latency. Profile validation overhead and optimize hot paths if needed. Consider caching parsed results when appropriate.
Wrap fetch calls with validation. Create an API client that parses responses automatically. This ensures all API data is validated consistently across your application. Type-safe API clients catch breaking changes from backend updates.
Handle validation errors appropriately. Network errors, HTTP errors, and validation errors are different failure modes. Present them differently to users. Network errors might be retryable, validation errors indicate a bug.
Generate API types from OpenAPI specs or JSON Schema. Tools like openapi-typescript create TypeScript types from API documentation. Combine this with runtime validation for end-to-end type safety.
Version your schemas alongside API versions. When APIs evolve, old clients might receive new response formats. Validation catches incompatibilities. Either handle both formats or force users to upgrade.
Logging validation failures helps debug integration issues. When validation fails, log the schema path, expected type, and actual value. This pinpoints exactly what is wrong with the response. Include request IDs to correlate with backend logs.
Testing validation schemas is important. Write tests that verify valid data passes and invalid data fails with expected errors. Test edge cases like null, undefined, empty strings, and boundary values. Schemas are code and need testing like any other code.
Environment variables are strings at runtime. TypeScript types do not help. Use Zod to parse process.env into a typed configuration object. This validates that required variables exist and have correct formats.
Define an environment schema with all required variables. Mark optional variables explicitly. Use .transform() to parse numbers and booleans from strings. Environment variables are always strings, so parsing is necessary.
Fail fast if environment is invalid. Validate environment variables at application startup before any code runs. If validation fails, crash with a clear error message listing missing or invalid variables. This prevents confusing runtime errors later.
Provide sensible defaults for development. Use .default() for non-sensitive configuration. This lets developers run the application without extensive setup. But never default sensitive values like API keys.
Document environment variables using schema descriptions. Zod supports .describe() to add documentation to each field. Generate documentation from schemas to keep it in sync with actual requirements.
Different environments need different configurations. Use environment-specific schemas or conditional validation. Development might have relaxed requirements, production should be strict.
Discriminated unions handle polymorphic data. Use a discriminator field to determine which schema variant to apply. This is common with API responses that return different shapes based on a type field.
Recursive schemas validate tree-like structures. Zod supports lazy evaluation for recursive types. This is necessary for nested comments, file systems, or organizational hierarchies.
Coercion converts types during parsing. Use z.coerce.number() to parse strings as numbers. This is helpful for query parameters or form data that arrive as strings but represent numbers.
Branded types prevent mixing semantically different values. Create distinct types for user IDs and post IDs even though both are numbers. This catches bugs where IDs are accidentally swapped.
Schema versioning handles API evolution. Maintain schemas for each API version. Parse responses based on version header. This supports multiple API versions simultaneously.
Partial validation allows updating subsets of data. Use .partial() to make all fields optional. This is useful for PATCH endpoints that update specific fields without requiring entire object.
Strict mode rejects unknown keys. By default Zod allows extra properties. Use .strict() to fail validation if unexpected fields appear. This catches typos and API drift.
Preprocess data before validation. Use .preprocess() to normalize input. This is useful for trimming strings, converting formats, or handling legacy data.
Performance optimization for large datasets. Batch validation when processing arrays. Stream processing for huge files. Profile validation to identify bottlenecks.
Testing validation logic comprehensively. Unit test schemas with valid and invalid inputs. Test boundary conditions. Test error messages. Schemas are critical code paths.
Read more articles on the FlexKit blog