Blogs / Solving TypeScript Runtime Validation Without Changing Your Code
Solving TypeScript Runtime Validation Without Changing Your Code
August 23, 2025 • Matthew Duong • Engineering • 10 min read
Why I Built ts-runtime-validation
After years of wrestling with TypeScript’s disappearing type safety at runtime, I built ts-runtime-validation to solve a problem that kept frustrating me: why should I rewrite all my TypeScript interfaces just to validate data at runtime?
Why? What is the problem?
Like many TypeScript developers, I’ve been burned by the false sense of security that TypeScript’s compile-time types provide. Your perfectly typed interfaces mean nothing when parsing API responses, reading user input, or loading configuration files. The data you receive might be completely different from what TypeScript expects, leading to runtime errors that TypeScript couldn’t prevent.
interface User {
id: string;
email: string;
age: number;
}
// TypeScript is happy, but this is a lie
const user: User = JSON.parse(userDataFromAPI);
console.log(user.email.toLowerCase()); // 💥 Runtime error if email is undefined
Why Existing Solutions Frustrated Me

When I first encountered this problem, I looked at the popular runtime validation libraries. Zod dominates the space with 39,487 GitHub stars and 39.6 million weekly downloads12. Yup, io-ts, and others offer similar approaches. They’re all excellent tools, but they share a fundamental approach that frustrated me: you have to redefine your types using their APIs.
// With Zod - you define the schema, not the type
const UserSchema = z.object({
id: z.string(),
email: z.string(),
age: z.number(),
});
type User = z.infer<typeof UserSchema>; // Type is derived from schema
This meant I’d have to:
- Rewrite hundreds of existing TypeScript interfaces across multiple projects
- Learn each library’s schema definition API
- Maintain schema definitions alongside (or instead of) TypeScript types
- Add runtime dependencies that increase bundle size (Zod is ~12KB gzipped3, Yup is ~60KB4)
I kept thinking: I already have TypeScript interfaces. Why can’t I just use those?
My Approach
Instead of changing how I write TypeScript, I built ts-runtime-validation to work with the TypeScript I already had. The approach is simple:
- Keep your existing TypeScript interfaces unchanged
- Add validation rules using JSDoc comments (optional)
- Generate validators at build time
- Get type-safe runtime validation with zero runtime overhead
Here’s how it works in practice:
// user.jsonschema.ts - Your existing TypeScript remains unchanged
export interface User {
/**
* MongoDB ObjectId
* @pattern ^[a-fA-F0-9]{24}$
*/
id: string;
/**
* @format email
*/
email: string;
/**
* @minimum 0
* @maximum 150
*/
age: number;
}
Run the generator:
npx ts-runtime-validation
And use the generated type-safe validator:
import { isValidSchema } from "./.ts-runtime-validation/isValidSchema";
const userData = await fetch("/api/user").then((r) => r.json());
if (isValidSchema(userData, "#/definitions/User")) {
// userData is now typed as User
console.log(userData.email.toLowerCase()); // Safe! TypeScript knows email exists
} else {
console.error("Invalid user data");
}
Generated Files: The tool creates optimized files in your output directory:
| File | Description | Optimizations |
|---|---|---|
validation.schema.json | JSON Schema definitions for all your types | Minification with --minify |
SchemaDefinition.ts | TypeScript interface mapping schema paths to types | Tree-shaking ready imports |
isValidSchema.ts | Type guard helper with runtime validation | Lazy loading with --lazy-load |
ValidationType.ts | Centralized type exports | Individual exports or namespace |
The Architecture
My implementation leverages three battle-tested libraries:
- ts-json-schema-generator: Parses TypeScript AST and converts interfaces to JSON Schema5
- ts-morph: Provides TypeScript compiler API access for analyzing your code6
- AJV: The fastest JSON Schema validator, processing objects in ~42 nanoseconds7
The architecture is straightforward:
TypeScript Interfaces → JSON Schema → TypeScript Type Guards

This build-time generation approach means:
- No runtime schema construction overhead
- Minimal runtime dependencies (just AJV for validation)
- Your production bundle only includes the generated validators, not the generation logic
Modern Architecture: The tool uses a service-oriented architecture optimized for performance:
- FileDiscovery: Intelligent file system operations with caching
- SchemaProcessor: TypeScript AST processing with parallel execution
- CodeGenerator: Optimized TypeScript file generation
- SchemaWriter: Efficient file writing with minification support
- ProgressReporter: User-friendly progress tracking for long operations
Real-World Usage: Validating Fields in DS160.io

I’ve been dogfooding ts-runtime-validation in my visa application tool, DS160.io. The DS-160 form has over 500 fields with complex validation rules. Procedure:
- Defined all form fields as TypeScript interfaces
- Added validation constraints using JSDoc annotations
- Generated validators for form submissions
- Achieved type-safe validation without writing any schema code
Here’s a real example from the codebase:
export interface IPassportInfo {
/**
* @pattern ^[A-Z0-9]{6,9}$
*/
passportNumber: string;
/**
* @format date
*/
issuanceDate: string;
/**
* @format date
*/
expirationDate: string;
/**
* ISO 3166-1 alpha-2 country code
* @pattern ^[A-Z]{2}$
*/
issuanceCountry: string;
}
The tool now supports comprehensive JSDoc annotations for validation rules:
Supported Annotations:
- Strings:
@minLength,@maxLength,@pattern,@format(email, uri, uuid, date-time, etc.) - Numbers:
@minimum,@maximum,@exclusiveMinimum,@exclusiveMaximum,@multipleOf - Arrays:
@minItems,@maxItems,@uniqueItems - Objects:
@minProperties,@maxProperties - General:
@description,@default,@examples
// Advanced validation example
export interface IAdvancedProduct {
/**
* Product tags
* @minItems 1
* @maxItems 10
* @uniqueItems true
*/
tags: string[];
/**
* Product price in cents
* @minimum 0
* @maximum 1000000
* @multipleOf 1
*/
price: number;
/**
* Product website
* @format uri
* @pattern ^https://
*/
website?: string;
}
The generated validators catch invalid data before it reaches the backend, providing better user experience and reducing server load.
Performance: How My Approach Compares
After analyzing the current landscape and running benchmarks, here’s how ts-runtime-validation compares:
Validation Speed
My approach uses AJV under the hood, which leads in raw validation performance8:
- AJV (my approach): ~42 nanoseconds per validation
- Zod: 200-300 nanoseconds (5x slower)
- Yup: 500+ nanoseconds (12x slower)
Bundle Size
Since validators are pre-generated, the runtime impact is minimal9:
- ts-runtime-validation: 12KB (AJV only)
- Zod: 12KB (after recent optimizations)10
- Yup: 60KB
- io-ts: 35KB
Build-Time vs Runtime
The key difference is when the work happens:
- My approach: Schema construction happens at build time (0ms runtime overhead)
- Traditional libraries: Schema construction happens at runtime (12-18ms initial setup)11
When to Use ts-runtime-validation vs Other Libraries
Through building and using this tool, I’ve learned when different approaches make sense:
Use ts-runtime-validation when:
✅ You have existing TypeScript interfaces you don’t want to rewrite
✅ You prefer build-time code generation over runtime libraries
✅ You want minimal runtime dependencies
✅ Your types don’t change frequently at runtime
✅ You’re already using a build step (TypeScript compilation)
Consider other libraries when:
❌ You need dynamic schema composition at runtime
❌ You prefer defining schemas first and deriving types
❌ You need complex runtime transformations or coercions
❌ You’re in a JavaScript-only environment
Advanced CLI Options and Performance Features
The tool now includes comprehensive performance optimizations and developer experience improvements:
Performance Options:
# Development workflow with caching and progress
ts-runtime-validation --cache --progress --verbose
# Production build with optimizations
ts-runtime-validation --cache --minify --tree-shaking
# Large projects with memory efficiency
ts-runtime-validation --cache --lazy-load --progress
Key Performance Features:
- Incremental builds - MD5-based file change detection with caching system
- Parallel processing - Concurrent file processing (can be disabled with
--no-parallel) - Bundle optimization - Tree-shaking friendly exports and minified output
- Memory efficiency - Lazy-loaded validators for large projects
- Progress tracking - Visual feedback for long-running operations
The caching system stores MD5 hashes in .ts-runtime-validation-cache/ and only processes modified files, providing significant speedup for large codebases.
Development Workflows:
{
"scripts": {
"generate-types": "ts-runtime-validation --cache --progress",
"generate-types:watch": "nodemon --watch 'src/**/*.jsonschema.ts' --exec 'yarn generate-types'",
"generate-types:dev": "ts-runtime-validation --cache --verbose --progress",
"generate-types:prod": "ts-runtime-validation --cache --minify --tree-shaking"
}
}
What alternatives are available
After researching the ecosystem extensively, I’ve found that each approach has its place:
Zod excels at developer experience with its TypeScript-first design and automatic type inference through z.infer<>. It’s become the de facto standard for modern TypeScript projects, especially when using tRPC12.
Valibot takes a modular approach, achieving bundles as small as 513 bytes through aggressive tree-shaking. For client-side applications where every byte counts, it can be 90% smaller than Zod13.
TypeBox generates standard JSON Schema like my approach but defines schemas at runtime. It’s becoming popular for OpenAPI documentation and achieves excellent performance through JIT compilation14.
Typia represents the cutting edge, using TypeScript transformers to generate validation code at compile time. It claims 20,000x performance improvements over traditional validators15.
My goals and learnings from this experience
-
Developer ergonomics matter more than features: The main value isn’t that it validates data (many tools do that), but that it works with existing code without changes.
-
Build-time complexity is better than runtime complexity: Moving complexity to build time means simpler, faster production code.
-
JSDoc is underutilized: Using JSDoc for validation rules keeps the configuration close to the code and doesn’t require learning a new API.
-
The TypeScript AST is powerful: Tools like ts-morph make it surprisingly easy to analyze and generate TypeScript code.
-
Performance optimizations matter: Features like incremental builds, parallel processing, and intelligent caching dramatically improve developer experience, especially for large projects.
Current Limitations
- No duplicate type names - Each interface/type must have a unique name across all schema files
- TypeScript-only constructs - Some advanced TypeScript features (like conditional types) may not be fully supported
- Circular references - Limited support for circular type references
- Build step requirement - Must regenerate when types change (though caching minimizes this impact)
Try It Yourself
Give ts-runtime-validation a try:
# Using yarn (recommended)
yarn add --dev ts-runtime-validation
yarn add ajv # Required peer dependency
# Using npm
npm install --save-dev ts-runtime-validation
npm install ajv # Required peer dependency
Check out the complete documentation with interactive examples and quick start guide, or visit the GitHub repository for the source code.
References
Footnotes
-
Zod GitHub Repository - 39,487 stars as of January 2025 ↩
-
NPM Weekly Downloads Comparison - Download statistics for validation libraries ↩
-
Zod Bundle Size Analysis - Bundle size metrics ↩
-
Validation Library Bundle Sizes - Comprehensive size comparison ↩
-
ts-json-schema-generator - TypeScript to JSON Schema converter ↩
-
ts-morph Documentation - TypeScript compiler API wrapper ↩
-
Node.js Validators Benchmark - Performance comparison ↩
-
AJV vs Zod Performance Analysis - Runtime validator comparison ↩
-
Schema Validation Libraries Comparison - AJV, Joi, Yup, and Zod analysis ↩
-
Zod Performance Improvements - Recent optimization details ↩
-
TypeScript Runtime Validators Performance - Type-checking performance analysis ↩
-
Zod Documentation - Official Zod documentation ↩
-
Introducing Valibot - Valibot’s modular approach ↩
-
TypeBox vs Zod Comparison - Why Val Town uses TypeBox ↩
-
Typia Performance Claims - Compile-time validation benchmarks ↩