mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
📝 docs: add reference documentation for specialist skills
Add migration-patterns.md for test-migration-specialist (Jest to Vitest) and zod-patterns.md for type-safety-engineer (Zod validation patterns).
This commit is contained in:
@@ -0,0 +1,346 @@
|
|||||||
|
# Jest to Vitest Migration Patterns
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This reference provides syntax mappings and patterns for migrating tests from Jest (with Spectator) to Vitest (with Angular Testing Library).
|
||||||
|
|
||||||
|
## Configuration Migration
|
||||||
|
|
||||||
|
### Jest Config → Vitest Config
|
||||||
|
|
||||||
|
**Before (jest.config.ts):**
|
||||||
|
```typescript
|
||||||
|
export default {
|
||||||
|
displayName: 'my-lib',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
|
coverageDirectory: '../../coverage/libs/my-lib',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||||
|
snapshotSerializers: [
|
||||||
|
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||||
|
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (vitest.config.ts):**
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [angular()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['src/test-setup.ts'],
|
||||||
|
include: ['**/*.spec.ts'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Setup Migration
|
||||||
|
|
||||||
|
**Before (test-setup.ts - Jest):**
|
||||||
|
```typescript
|
||||||
|
import 'jest-preset-angular/setup-jest';
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (test-setup.ts - Vitest):**
|
||||||
|
```typescript
|
||||||
|
import '@analogjs/vitest-angular/setup-zone';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Changes
|
||||||
|
|
||||||
|
### Test Function Imports
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mock Imports
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
jest.fn()
|
||||||
|
jest.spyOn()
|
||||||
|
jest.mock()
|
||||||
|
jest.useFakeTimers()
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
vi.fn()
|
||||||
|
vi.spyOn()
|
||||||
|
vi.mock()
|
||||||
|
vi.useFakeTimers()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mock Migration Patterns
|
||||||
|
|
||||||
|
### Function Mocks
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const mockFnWithReturn = jest.fn().mockReturnValue('value');
|
||||||
|
const mockFnWithAsync = jest.fn().mockResolvedValue('async value');
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
const mockFn = vi.fn();
|
||||||
|
const mockFnWithReturn = vi.fn().mockReturnValue('value');
|
||||||
|
const mockFnWithAsync = vi.fn().mockResolvedValue('async value');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spy Migration
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
const spy = jest.spyOn(service, 'method');
|
||||||
|
spy.mockImplementation(() => 'mocked');
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
const spy = vi.spyOn(service, 'method');
|
||||||
|
spy.mockImplementation(() => 'mocked');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Mocks
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
jest.mock('@isa/core/logging', () => ({
|
||||||
|
logger: jest.fn(() => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
vi.mock('@isa/core/logging', () => ({
|
||||||
|
logger: vi.fn(() => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spectator → Angular Testing Library
|
||||||
|
|
||||||
|
### Component Testing
|
||||||
|
|
||||||
|
**Before (Spectator):**
|
||||||
|
```typescript
|
||||||
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||||
|
|
||||||
|
describe('MyComponent', () => {
|
||||||
|
let spectator: Spectator<MyComponent>;
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: MyComponent,
|
||||||
|
imports: [CommonModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: MyService, useValue: mockService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
expect(spectator.query('.title')).toHaveText('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle click', () => {
|
||||||
|
spectator.click('.button');
|
||||||
|
expect(mockService.doSomething).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Angular Testing Library):**
|
||||||
|
```typescript
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/angular';
|
||||||
|
|
||||||
|
describe('MyComponent', () => {
|
||||||
|
it('should render title', async () => {
|
||||||
|
await render(MyComponent, {
|
||||||
|
imports: [CommonModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: MyService, useValue: mockService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle click', async () => {
|
||||||
|
await render(MyComponent, {
|
||||||
|
providers: [{ provide: MyService, useValue: mockService }],
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button'));
|
||||||
|
expect(mockService.doSomething).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Selectors
|
||||||
|
|
||||||
|
| Spectator | Angular Testing Library |
|
||||||
|
|-----------|------------------------|
|
||||||
|
| `spectator.query('.class')` | `screen.getByTestId()` or `screen.getByRole()` |
|
||||||
|
| `spectator.queryAll('.class')` | `screen.getAllByRole()` |
|
||||||
|
| `spectator.query('button')` | `screen.getByRole('button')` |
|
||||||
|
| `spectator.query('[data-testid]')` | `screen.getByTestId()` |
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
| Spectator | Angular Testing Library |
|
||||||
|
|-----------|------------------------|
|
||||||
|
| `spectator.click(element)` | `fireEvent.click(element)` or `await userEvent.click(element)` |
|
||||||
|
| `spectator.typeInElement(value, element)` | `await userEvent.type(element, value)` |
|
||||||
|
| `spectator.blur(element)` | `fireEvent.blur(element)` |
|
||||||
|
| `spectator.focus(element)` | `fireEvent.focus(element)` |
|
||||||
|
|
||||||
|
### Service Testing
|
||||||
|
|
||||||
|
**Before (Spectator):**
|
||||||
|
```typescript
|
||||||
|
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||||
|
|
||||||
|
describe('MyService', () => {
|
||||||
|
let spectator: SpectatorService<MyService>;
|
||||||
|
const createService = createServiceFactory({
|
||||||
|
service: MyService,
|
||||||
|
providers: [
|
||||||
|
{ provide: HttpClient, useValue: mockHttp },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spectator = createService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch data', () => {
|
||||||
|
spectator.service.getData().subscribe(data => {
|
||||||
|
expect(data).toEqual(expectedData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (TestBed):**
|
||||||
|
```typescript
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
describe('MyService', () => {
|
||||||
|
let service: MyService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
MyService,
|
||||||
|
{ provide: HttpClient, useValue: mockHttp },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(MyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch data', () => {
|
||||||
|
service.getData().subscribe(data => {
|
||||||
|
expect(data).toEqual(expectedData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async Testing
|
||||||
|
|
||||||
|
### Observable Testing
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
it('should emit values', (done) => {
|
||||||
|
service.data$.subscribe({
|
||||||
|
next: (value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
it('should emit values', async () => {
|
||||||
|
const value = await firstValueFrom(service.data$);
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timer Mocks
|
||||||
|
|
||||||
|
**Before (Jest):**
|
||||||
|
```typescript
|
||||||
|
jest.useFakeTimers();
|
||||||
|
service.startTimer();
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
jest.useRealTimers();
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Vitest):**
|
||||||
|
```typescript
|
||||||
|
vi.useFakeTimers();
|
||||||
|
service.startTimer();
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
vi.useRealTimers();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Matchers
|
||||||
|
|
||||||
|
| Jest | Vitest |
|
||||||
|
|------|--------|
|
||||||
|
| `expect(x).toBe(y)` | Same |
|
||||||
|
| `expect(x).toEqual(y)` | Same |
|
||||||
|
| `expect(x).toHaveBeenCalled()` | Same |
|
||||||
|
| `expect(x).toHaveBeenCalledWith(y)` | Same |
|
||||||
|
| `expect(x).toMatchSnapshot()` | `expect(x).toMatchSnapshot()` |
|
||||||
|
| `expect(x).toHaveText('text')` | `expect(x).toHaveTextContent('text')` (with jest-dom) |
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
1. [ ] Update `vitest.config.ts`
|
||||||
|
2. [ ] Update `test-setup.ts`
|
||||||
|
3. [ ] Replace `jest.fn()` with `vi.fn()`
|
||||||
|
4. [ ] Replace `jest.spyOn()` with `vi.spyOn()`
|
||||||
|
5. [ ] Replace `jest.mock()` with `vi.mock()`
|
||||||
|
6. [ ] Replace Spectator with Angular Testing Library
|
||||||
|
7. [ ] Update queries to use accessible selectors
|
||||||
|
8. [ ] Update async patterns
|
||||||
|
9. [ ] Run tests and fix any remaining issues
|
||||||
|
10. [ ] Remove Jest dependencies from `package.json`
|
||||||
293
.claude/skills/type-safety-engineer/references/zod-patterns.md
Normal file
293
.claude/skills/type-safety-engineer/references/zod-patterns.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Zod Patterns Reference
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Zod is a TypeScript-first schema validation library. Use it for runtime validation at system boundaries (API responses, form inputs, external data).
|
||||||
|
|
||||||
|
## Basic Schema Patterns
|
||||||
|
|
||||||
|
### Primitive Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Basic types
|
||||||
|
const stringSchema = z.string();
|
||||||
|
const numberSchema = z.number();
|
||||||
|
const booleanSchema = z.boolean();
|
||||||
|
const dateSchema = z.date();
|
||||||
|
|
||||||
|
// With constraints
|
||||||
|
const emailSchema = z.string().email();
|
||||||
|
const positiveNumber = z.number().positive();
|
||||||
|
const nonEmptyString = z.string().min(1);
|
||||||
|
const optionalString = z.string().optional();
|
||||||
|
const nullableString = z.string().nullable();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Object Schemas
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic object
|
||||||
|
const userSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string().min(1),
|
||||||
|
age: z.number().int().positive().optional(),
|
||||||
|
role: z.enum(['admin', 'user', 'guest']),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Infer TypeScript type from schema
|
||||||
|
type User = z.infer<typeof userSchema>;
|
||||||
|
|
||||||
|
// Partial and Pick utilities
|
||||||
|
const partialUser = userSchema.partial(); // All fields optional
|
||||||
|
const requiredUser = userSchema.required(); // All fields required
|
||||||
|
const pickedUser = userSchema.pick({ email: true, name: true });
|
||||||
|
const omittedUser = userSchema.omit({ createdAt: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array Schemas
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic array
|
||||||
|
const stringArray = z.array(z.string());
|
||||||
|
|
||||||
|
// With constraints
|
||||||
|
const nonEmptyArray = z.array(z.string()).nonempty();
|
||||||
|
const limitedArray = z.array(z.number()).min(1).max(10);
|
||||||
|
|
||||||
|
// Tuple (fixed length, different types)
|
||||||
|
const coordinate = z.tuple([z.number(), z.number()]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Response Validation
|
||||||
|
|
||||||
|
### Pattern: Validate API Responses
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define response schema
|
||||||
|
const apiResponseSchema = z.object({
|
||||||
|
data: z.object({
|
||||||
|
items: z.array(userSchema),
|
||||||
|
total: z.number(),
|
||||||
|
page: z.number(),
|
||||||
|
pageSize: z.number(),
|
||||||
|
}),
|
||||||
|
meta: z.object({
|
||||||
|
timestamp: z.string().datetime(),
|
||||||
|
requestId: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// In Angular service
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class UserService {
|
||||||
|
#http = inject(HttpClient);
|
||||||
|
#logger = logger({ component: 'UserService' });
|
||||||
|
|
||||||
|
getUsers(): Observable<User[]> {
|
||||||
|
return this.#http.get('/api/users').pipe(
|
||||||
|
map((response) => {
|
||||||
|
const result = apiResponseSchema.safeParse(response);
|
||||||
|
if (!result.success) {
|
||||||
|
this.#logger.error('Invalid API response', {
|
||||||
|
errors: result.error.errors
|
||||||
|
});
|
||||||
|
throw new Error('Invalid API response');
|
||||||
|
}
|
||||||
|
return result.data.data.items;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Coerce Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API returns string IDs, coerce to number
|
||||||
|
const productSchema = z.object({
|
||||||
|
id: z.coerce.number(), // "123" -> 123
|
||||||
|
price: z.coerce.number(), // "99.99" -> 99.99
|
||||||
|
inStock: z.coerce.boolean(), // "true" -> true
|
||||||
|
createdAt: z.coerce.date(), // "2024-01-01" -> Date
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Validation
|
||||||
|
|
||||||
|
### Pattern: Form Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define form schema with custom error messages
|
||||||
|
const loginFormSchema = z.object({
|
||||||
|
email: z.string()
|
||||||
|
.email({ message: 'Invalid email address' }),
|
||||||
|
password: z.string()
|
||||||
|
.min(8, { message: 'Password must be at least 8 characters' })
|
||||||
|
.regex(/[A-Z]/, { message: 'Must contain uppercase letter' })
|
||||||
|
.regex(/[0-9]/, { message: 'Must contain number' }),
|
||||||
|
rememberMe: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use with Angular forms
|
||||||
|
type LoginForm = z.infer<typeof loginFormSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Cross-field Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const passwordFormSchema = z.object({
|
||||||
|
password: z.string().min(8),
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
}).refine(
|
||||||
|
(data) => data.password === data.confirmPassword,
|
||||||
|
{
|
||||||
|
message: "Passwords don't match",
|
||||||
|
path: ['confirmPassword'], // Error path
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Guards
|
||||||
|
|
||||||
|
### Pattern: Create Type Guard from Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Schema
|
||||||
|
const customerSchema = z.object({
|
||||||
|
type: z.literal('customer'),
|
||||||
|
customerId: z.string(),
|
||||||
|
loyaltyPoints: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type guard function
|
||||||
|
function isCustomer(value: unknown): value is z.infer<typeof customerSchema> {
|
||||||
|
return customerSchema.safeParse(value).success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
if (isCustomer(data)) {
|
||||||
|
console.log(data.loyaltyPoints); // Type-safe access
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Discriminated Unions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const customerSchema = z.object({
|
||||||
|
type: z.literal('customer'),
|
||||||
|
customerId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const guestSchema = z.object({
|
||||||
|
type: z.literal('guest'),
|
||||||
|
sessionId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSchema = z.discriminatedUnion('type', [
|
||||||
|
customerSchema,
|
||||||
|
guestSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
type User = z.infer<typeof userSchema>;
|
||||||
|
// User = { type: 'customer'; customerId: string } | { type: 'guest'; sessionId: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Replacing `any` Types
|
||||||
|
|
||||||
|
### Before (unsafe)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function processOrder(order: any) {
|
||||||
|
// No type safety
|
||||||
|
console.log(order.items.length);
|
||||||
|
console.log(order.customer.name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (with Zod)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const orderSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
items: z.array(z.object({
|
||||||
|
productId: z.string(),
|
||||||
|
quantity: z.number().positive(),
|
||||||
|
price: z.number().nonnegative(),
|
||||||
|
})),
|
||||||
|
customer: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
}),
|
||||||
|
status: z.enum(['pending', 'confirmed', 'shipped', 'delivered']),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Order = z.infer<typeof orderSchema>;
|
||||||
|
|
||||||
|
function processOrder(input: unknown): Order {
|
||||||
|
const order = orderSchema.parse(input); // Throws if invalid
|
||||||
|
console.log(order.items.length); // Type-safe
|
||||||
|
console.log(order.customer.name); // Type-safe
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Pattern: Structured Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = schema.safeParse(data);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
// Access formatted errors
|
||||||
|
const formatted = result.error.format();
|
||||||
|
|
||||||
|
// Access flat error list
|
||||||
|
const flat = result.error.flatten();
|
||||||
|
|
||||||
|
// Custom error mapping
|
||||||
|
const errors = result.error.errors.map(err => ({
|
||||||
|
field: err.path.join('.'),
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transform Patterns
|
||||||
|
|
||||||
|
### Pattern: Transform on Parse
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const userInputSchema = z.object({
|
||||||
|
email: z.string().email().transform(s => s.toLowerCase()),
|
||||||
|
name: z.string().transform(s => s.trim()),
|
||||||
|
tags: z.string().transform(s => s.split(',')),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Input: { email: "USER@EXAMPLE.COM", name: " John ", tags: "a,b,c" }
|
||||||
|
// Output: { email: "user@example.com", name: "John", tags: ["a", "b", "c"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Default Values
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const configSchema = z.object({
|
||||||
|
theme: z.enum(['light', 'dark']).default('light'),
|
||||||
|
pageSize: z.number().default(20),
|
||||||
|
features: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Define schemas at module boundaries** - API services, form handlers
|
||||||
|
2. **Use `safeParse` for error handling** - Don't let validation throw unexpectedly
|
||||||
|
3. **Infer types from schemas** - Single source of truth
|
||||||
|
4. **Add meaningful error messages** - Help debugging and user feedback
|
||||||
|
5. **Use transforms for normalization** - Clean data on parse
|
||||||
|
6. **Keep schemas close to usage** - Colocate with services/components
|
||||||
Reference in New Issue
Block a user