Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:35 +08:00
commit 6fcffca9b0
35 changed files with 8235 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
---
name: using-runtime-checks
description: Teaches how to validate external data at runtime using Zod and other validation libraries in TypeScript. Use when working with APIs, JSON parsing, user input, or any external data source where runtime validation is needed.
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
version: 1.0.0
---
<role>
This skill teaches runtime validation patterns using libraries like Zod to bridge the gap between TypeScript's compile-time types and runtime realities. Critical for preventing runtime errors from unvalidated external data.
</role>
<when-to-activate>
This skill activates when:
- Working with API responses or external data
- Parsing JSON from unknown sources
- Handling user input or form data
- Integrating with third-party services
- User mentions validation, Zod, io-ts, runtime checks, or external data
</when-to-activate>
<overview>
**Critical Insight**: TypeScript types are erased at compile time. At runtime, you have no type safety for external data.
```typescript
const data: User = await fetch("/api/user").then(r => r.json());
```
This compiles, but if the API returns `{ username: string }` instead of `{ name: string }`, your code crashes at runtime.
**Solution**: Runtime validation libraries that:
1. Validate data structure at runtime
2. Provide TypeScript types automatically
3. Generate helpful error messages
**Recommended Library**: Zod (modern, TypeScript-first, best DX)
</overview>
<workflow>
## Runtime Validation Flow
**Step 1: Define Schema**
Create a schema describing the expected shape:
```typescript
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().int().positive()
});
```
**Step 2: Extract TypeScript Type**
```typescript
type User = z.infer<typeof UserSchema>;
```
**Step 3: Validate at Runtime**
```typescript
const data = await fetch("/api/user").then(r => r.json());
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
console.log(user.name);
} else {
console.error("Validation failed:", result.error);
}
```
**Step 4: Handle Validation Errors**
```typescript
if (!result.success) {
const issues = result.error.issues.map(issue => ({
path: issue.path.join("."),
message: issue.message
}));
throw new ValidationError("Invalid user data", issues);
}
```
</workflow>
<examples>
## Example 1: API Response Validation
**Setup**
```typescript
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "guest"]),
createdAt: z.string().datetime()
});
type User = z.infer<typeof UserSchema>;
```
**Validation**
```typescript
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data: unknown = await response.json();
const result = UserSchema.safeParse(data);
if (!result.success) {
throw new Error(`Invalid user data: ${result.error.message}`);
}
return result.data;
}
```
---
## Example 2: Form Input Validation
```typescript
const LoginFormSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
rememberMe: z.boolean().optional()
});
type LoginForm = z.infer<typeof LoginFormSchema>;
function validateLoginForm(formData: unknown): LoginForm {
return LoginFormSchema.parse(formData);
}
const form = {
email: "user@example.com",
password: "securepassword123"
};
try {
const validated = validateLoginForm(form);
await login(validated);
} catch (error) {
if (error instanceof z.ZodError) {
error.issues.forEach(issue => {
console.error(`${issue.path.join(".")}: ${issue.message}`);
});
}
}
```
---
## Example 3: Nested Object Validation
```typescript
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
});
const UserWithAddressSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
address: AddressSchema,
billingAddress: AddressSchema.optional()
});
type UserWithAddress = z.infer<typeof UserWithAddressSchema>;
```
---
## Example 4: Array Validation
```typescript
const TagSchema = z.string().min(1).max(20);
const PostSchema = z.object({
id: z.string(),
title: z.string().min(1).max(200),
content: z.string(),
tags: z.array(TagSchema).min(1).max(10),
metadata: z.record(z.string(), z.unknown())
});
type Post = z.infer<typeof PostSchema>;
async function fetchPosts(): Promise<Post[]> {
const response = await fetch("/api/posts");
const data: unknown = await response.json();
const PostsSchema = z.array(PostSchema);
return PostsSchema.parse(data);
}
```
---
## Example 5: Advanced Patterns
**Union Types**: Use discriminated unions for API responses with success/error states.
**Transforms**: Convert strings to dates or coerce query parameters to numbers.
**Refinements**: Add custom validation logic for complex business rules (e.g., password strength).
**Partial Schemas**: Create update schemas from full schemas using `.partial()`.
See `references/zod-patterns.md` for complete examples of unions, transforms, refinements, and partial schemas.
</examples>
<progressive-disclosure>
## Reference Files
For complete patterns see `references/`:
- `references/zod-patterns.md` - Complete Zod API reference
- `references/error-handling.md` - Validation error handling strategies
- `references/performance.md` - Validation performance optimization
For related skills:
- **Type Guards**: Use the using-type-guards skill for manual type narrowing
- **Unknown vs Any**: Use the avoiding-any-types skill for why validation is needed
- **External Data**: Use the validating-external-data skill for specific data source patterns
**Cross-Plugin References:**
- If constructing Zod schemas for runtime validation, use the validating-schema-basics skill for type-safe Zod v4 schema patterns
- If handling validation errors, use the customizing-errors skill for error formatting and custom messages
</progressive-disclosure>
<constraints>
**MUST:**
- Validate all external data at runtime (APIs, JSON, user input)
- Use `safeParse` for error handling, not `parse` (unless throwing is desired)
- Handle validation errors gracefully with user-friendly messages
- Validate before type assertions
**SHOULD:**
- Use Zod for new projects (best TypeScript integration)
- Define schemas close to usage
- Reuse schemas for related structures
- Transform data during validation when beneficial
**NEVER:**
- Trust external data without validation
- Use type assertions (`as Type`) on unvalidated data
- Ignore validation errors
- Validate in multiple places (validate at boundaries)
- Skip validation for "trusted" sources (trust issues change)
</constraints>
<installation>
## Installing Zod
```bash
npm install zod
pnpm add zod
yarn add zod
```
TypeScript configuration:
```json
{
"compilerOptions": {
"strict": true
}
}
```
</installation>
<validation>
## Validation Implementation Checklist
**Schema Definition**: Match expected structure, use appropriate validators, add custom refinements.
**Error Handling**: Use `safeParse`, log errors with context, provide user-friendly messages.
**Type Safety**: Derive types with `z.infer`, avoid manual assertions after validation.
**Performance**: Reuse schemas, validate at boundaries only, avoid redundant checks.
**Testing**: Test valid data, invalid data, and edge cases.
See `references/error-handling.md` for error handling patterns and `references/performance.md` for optimization techniques.
</validation>
<alternatives>
## Other Validation Libraries
**io-ts** (functional programming style)
```typescript
import * as t from "io-ts";
const UserCodec = t.type({
id: t.string,
name: t.string,
email: t.string
});
type User = t.TypeOf<typeof UserCodec>;
```
**Yup** (common with Formik)
```typescript
import * as yup from "yup";
const userSchema = yup.object({
name: yup.string().required(),
email: yup.string().email().required()
});
```
**AJV** (JSON Schema)
```typescript
import Ajv from "ajv";
const ajv = new Ajv();
const validate = ajv.compile({
type: "object",
properties: {
name: { type: "string" },
email: { type: "string", format: "email" }
},
required: ["name", "email"]
});
```
**Recommendation**: Use Zod for new TypeScript projects. Best DX and type inference.
</alternatives>
<common-patterns>
## Common Patterns
**Validation Middleware**: Validate request bodies in Express/framework handlers before processing.
**Safe JSON Parse**: Combine JSON.parse with schema validation for type-safe parsing.
**Configuration Validation**: Validate environment variables and config at startup.
See `references/zod-patterns.md` for complete implementations and additional patterns.
</common-patterns>

View File

@@ -0,0 +1,175 @@
# Validation Error Handling
## Basic Error Handling
### Using safeParse
```typescript
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
console.log(user.name);
} else {
console.error("Validation failed:", result.error);
}
```
### Using parse (throwing)
```typescript
try {
const user = UserSchema.parse(data);
console.log(user.name);
} catch (error) {
if (error instanceof z.ZodError) {
console.error("Validation failed:", error.issues);
}
}
```
## Processing Validation Issues
### Extracting Error Details
```typescript
if (!result.success) {
const issues = result.error.issues.map(issue => ({
path: issue.path.join("."),
message: issue.message,
code: issue.code
}));
throw new ValidationError("Invalid user data", issues);
}
```
### User-Friendly Error Messages
```typescript
function formatValidationError(error: z.ZodError): string {
return error.issues
.map(issue => {
const field = issue.path.join(".");
return `${field}: ${issue.message}`;
})
.join(", ");
}
try {
const validated = validateLoginForm(form);
await login(validated);
} catch (error) {
if (error instanceof z.ZodError) {
const message = formatValidationError(error);
showError(message);
}
}
```
## Error Recovery Strategies
### Fallback Values
```typescript
const result = ConfigSchema.safeParse(data);
const config = result.success
? result.data
: getDefaultConfig();
```
### Partial Success
```typescript
function validateItemsWithPartialSuccess<T>(
items: unknown[],
schema: z.ZodType<T>
): { valid: T[], invalid: { item: unknown, error: z.ZodError }[] } {
const valid: T[] = [];
const invalid: { item: unknown, error: z.ZodError }[] = [];
for (const item of items) {
const result = schema.safeParse(item);
if (result.success) {
valid.push(result.data);
} else {
invalid.push({ item, error: result.error });
}
}
return { valid, invalid };
}
```
## Custom Error Classes
```typescript
class ValidationError extends Error {
constructor(
message: string,
public readonly issues: Array<{ path: string; message: string }>
) {
super(message);
this.name = "ValidationError";
}
}
function validateOrThrow<T>(data: unknown, schema: z.ZodType<T>): T {
const result = schema.safeParse(data);
if (!result.success) {
const issues = result.error.issues.map(issue => ({
path: issue.path.join("."),
message: issue.message
}));
throw new ValidationError("Validation failed", issues);
}
return result.data;
}
```
## Logging Validation Errors
```typescript
function validateWithLogging<T>(
data: unknown,
schema: z.ZodType<T>,
context: string
): T | null {
const result = schema.safeParse(data);
if (!result.success) {
logger.error({
context,
error: "Validation failed",
issues: result.error.issues,
data: sanitizeForLogging(data)
});
return null;
}
return result.data;
}
```
## API Error Responses
```typescript
app.post("/api/users", (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: "Invalid request body",
details: result.error.issues.map(issue => ({
field: issue.path.join("."),
message: issue.message
}))
});
}
const user = result.data;
});
```

View File

@@ -0,0 +1,211 @@
# Validation Performance Optimization
## Schema Reuse
**BAD: Creating schemas repeatedly**
```typescript
function validateUser(data: unknown) {
const UserSchema = z.object({
id: z.string(),
name: z.string()
});
return UserSchema.parse(data);
}
```
**GOOD: Reusing schemas**
```typescript
const UserSchema = z.object({
id: z.string(),
name: z.string()
});
function validateUser(data: unknown) {
return UserSchema.parse(data);
}
```
## Boundary Validation
Validate once at system boundaries, not internally:
```typescript
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
return UserSchema.parse(data);
}
function processUser(user: User) {
const fullName = formatName(user);
return fullName;
}
```
## Lazy Schema Definition
For schemas with circular dependencies:
```typescript
const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({
id: z.string(),
name: z.string(),
parent: CategorySchema.optional()
}));
```
## Selective Validation
Validate only what's needed:
```typescript
const FullUserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
profile: z.object({
bio: z.string(),
avatar: z.string().url()
}),
settings: z.object({
theme: z.enum(["light", "dark"]),
notifications: z.boolean()
})
});
const BasicUserSchema = FullUserSchema.pick({
id: true,
name: true,
email: true
});
```
## Batch Validation
For arrays, validate the array once rather than each item:
```typescript
const UsersSchema = z.array(UserSchema);
async function fetchAllUsers(): Promise<User[]> {
const response = await fetch("/api/users");
const data: unknown = await response.json();
return UsersSchema.parse(data);
}
```
## Early Validation
Fail fast on critical fields:
```typescript
const CriticalFirstSchema = z.object({
apiKey: z.string().uuid(),
timestamp: z.number()
}).strict();
const FullRequestSchema = CriticalFirstSchema.extend({
payload: z.unknown()
});
```
## Caching Parsed Results
```typescript
const schemaCache = new WeakMap<object, User>();
function getCachedUser(data: unknown): User {
if (typeof data === "object" && data !== null) {
const cached = schemaCache.get(data);
if (cached) return cached;
}
const user = UserSchema.parse(data);
if (typeof data === "object" && data !== null) {
schemaCache.set(data, user);
}
return user;
}
```
## Avoiding Redundant Validation
```typescript
class UserService {
private validated = new WeakSet<object>();
async processUser(data: unknown): Promise<void> {
if (typeof data !== "object" || data === null) {
throw new Error("Invalid data");
}
if (!this.validated.has(data)) {
UserSchema.parse(data);
this.validated.add(data);
}
const user = data as User;
}
}
```
## Performance Monitoring
```typescript
async function validateWithMetrics<T>(
data: unknown,
schema: z.ZodType<T>,
name: string
): Promise<T> {
const start = performance.now();
try {
const result = schema.parse(data);
const duration = performance.now() - start;
metrics.record(`validation.${name}.success`, duration);
return result;
} catch (error) {
const duration = performance.now() - start;
metrics.record(`validation.${name}.failure`, duration);
throw error;
}
}
```
## Alternative Libraries for Performance
For maximum performance in hot paths:
**TypeBox** (compile-time optimized):
```typescript
import { Type, Static } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
const UserType = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String({ format: "email" })
});
type User = Static<typeof UserType>;
const user = Value.Parse(UserType, data);
```
**Valibot** (smaller bundle size):
```typescript
import * as v from "valibot";
const UserSchema = v.object({
id: v.string(),
name: v.string(),
email: v.pipe(v.string(), v.email())
});
type User = v.InferOutput<typeof UserSchema>;
```

View File

@@ -0,0 +1,241 @@
# Zod Patterns Reference
## Advanced Schema Composition
### Union Types
```typescript
const SuccessResponseSchema = z.object({
status: z.literal("success"),
data: z.unknown()
});
const ErrorResponseSchema = z.object({
status: z.literal("error"),
error: z.string(),
code: z.number()
});
const ApiResponseSchema = z.discriminatedUnion("status", [
SuccessResponseSchema,
ErrorResponseSchema
]);
type ApiResponse = z.infer<typeof ApiResponseSchema>;
```
### Transform and Coerce
```typescript
const DateSchema = z.string().datetime().transform(str => new Date(str));
const UserWithDatesSchema = z.object({
id: z.string(),
name: z.string(),
createdAt: DateSchema,
updatedAt: DateSchema
});
type UserWithDates = z.infer<typeof UserWithDatesSchema>;
const CoerceNumberSchema = z.coerce.number();
const QueryParamsSchema = z.object({
page: CoerceNumberSchema.int().positive().default(1),
limit: CoerceNumberSchema.int().min(1).max(100).default(20),
sort: z.enum(["asc", "desc"]).default("asc")
});
```
### Custom Refinements
```typescript
const PasswordSchema = z
.string()
.min(8)
.refine(
password => /[A-Z]/.test(password),
"Password must contain at least one uppercase letter"
)
.refine(
password => /[a-z]/.test(password),
"Password must contain at least one lowercase letter"
)
.refine(
password => /[0-9]/.test(password),
"Password must contain at least one number"
);
const SignupSchema = z.object({
email: z.string().email(),
password: PasswordSchema,
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ["confirmPassword"]
}
);
```
### Partial and Optional Schemas
```typescript
const UpdateUserSchema = UserSchema.partial();
type UpdateUser = z.infer<typeof UpdateUserSchema>;
async function updateUser(id: string, updates: unknown): Promise<User> {
const validated = UpdateUserSchema.parse(updates);
const response = await fetch(`/api/users/${id}`, {
method: "PATCH",
body: JSON.stringify(validated)
});
return UserSchema.parse(await response.json());
}
```
### Nested Objects
```typescript
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
});
const UserWithAddressSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
address: AddressSchema,
billingAddress: AddressSchema.optional()
});
type UserWithAddress = z.infer<typeof UserWithAddressSchema>;
```
### Array Validation
```typescript
const TagSchema = z.string().min(1).max(20);
const PostSchema = z.object({
id: z.string(),
title: z.string().min(1).max(200),
content: z.string(),
tags: z.array(TagSchema).min(1).max(10),
metadata: z.record(z.string(), z.unknown())
});
type Post = z.infer<typeof PostSchema>;
async function fetchPosts(): Promise<Post[]> {
const response = await fetch("/api/posts");
const data: unknown = await response.json();
const PostsSchema = z.array(PostSchema);
return PostsSchema.parse(data);
}
```
## Generic Validation Helpers
```typescript
async function apiCall<T>(
endpoint: string,
dataSchema: z.ZodType<T>
): Promise<T> {
const response = await fetch(endpoint);
const rawData: unknown = await response.json();
const apiResponse = ApiResponseSchema.parse(rawData);
if (apiResponse.status === "error") {
throw new Error(`API Error ${apiResponse.code}: ${apiResponse.error}`);
}
return dataSchema.parse(apiResponse.data);
}
```
## Validation Middleware
```typescript
import { Request, Response, NextFunction } from "express";
function validateBody<T>(schema: z.ZodType<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: "Validation failed",
issues: result.error.issues
});
}
req.body = result.data;
next();
};
}
app.post("/users", validateBody(UserSchema), (req, res) => {
const user: User = req.body;
});
```
## Safe JSON Parse
```typescript
function safeJsonParse<T>(
json: string,
schema: z.ZodType<T>
): T {
try {
const data: unknown = JSON.parse(json);
return schema.parse(data);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error("Invalid JSON");
}
throw error;
}
}
```
## Configuration Validation
```typescript
const ConfigSchema = z.object({
port: z.number().int().positive().default(3000),
database: z.object({
host: z.string(),
port: z.number().int().positive(),
name: z.string()
}),
redis: z.object({
url: z.string().url()
}).optional()
});
type Config = z.infer<typeof ConfigSchema>;
function loadConfig(): Config {
const data: unknown = process.env;
return ConfigSchema.parse({
port: data.PORT,
database: {
host: data.DB_HOST,
port: data.DB_PORT,
name: data.DB_NAME
},
redis: data.REDIS_URL ? { url: data.REDIS_URL } : undefined
});
}
```