Best Practices and Comparisons
Best Practices
1. Choose the Right Definition Method
- ObjectType: Generally recommended, supports recursive references
- Struct: Union type variants, dynamic building
typescript
// ✅ Recommended: Complex models use ObjectType
class User extends ObjectType {
profile = Profile
posts = List(Post)
}
// ✅ Recommended: Union types use Struct
const Result = Union(
Struct({ success: Literal(true), data: User }),
Struct({ success: Literal(false), error: String })
)2. Use Discriminated Union Types
typescript
// ✅ Good: Use type field as discriminator
const Event = Union(
Struct({ type: Literal('click'), x: Number, y: Number }),
Struct({ type: Literal('keypress'), key: String }),
Struct({ type: Literal('scroll'), delta: Number })
)
// TypeScript can automatically narrow types
function handleEvent(event: TypeOf<typeof Event>) {
switch (event.type) {
case 'click':
console.log(event.x, event.y) // ✅ Type safe
break
}
}3. Compose Schema Operations
typescript
// Define complete model
class FullUser extends ObjectType {
id = String
name = String
email = String
password = String
role = String
createdAt = Date
updatedAt = Date
}
// Derive Schemas
const PublicUser = pickObject(FullUser, ['id', 'name'])
const CreateUserInput = omitObject(FullUser, ['id', 'createdAt', 'updatedAt'])
const UpdateUserInput = partialObject(CreateUserInput)
const AdminUser = FullUser // Admins can see complete information4. Use Specific Functions for Better Type Hints
typescript
// ❌ Not recommended: Generic functions lack field hints
const SafeUser = omit(FullUser, ['password'])
// ✅ Recommended: Specific functions have complete IDE hints
const SafeUser = omitObject(FullUser, ['password']) // IDE will hint all fields5. Create Reusable Validators
typescript
// Define reusable validators
const Email = class extends ValidatorType<string> {
validate(input: unknown) {
const result = Validator.validate(String, input)
if (result.kind === 'Err') return result
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(result.value)) {
return this.Err('Invalid email')
}
return this.Ok(result.value)
}
}
const Phone = RegExp(/^1[3-9]\d{9}$/)
const Username = RegExp(/^[a-zA-Z0-9_]{3,16}$/)
// Reuse in multiple Schemas
class User extends ObjectType {
username = Username
email = Email
phone = Phone
}
class ContactForm extends ObjectType {
email = Email
phone = Phone
}6. Error Handling
typescript
import { Result } from 'farrow-schema'
// Use Result type for unified error handling
function validateAndCreate(data: unknown): Result<User, string> {
const result = Validator.validate(CreateUserInput, data)
// Use kind for type narrowing
if (result.kind === 'Err') {
return Err(`Validation failed: ${result.value.message}`)
}
// Business logic validation
if (result.value.age < 18) {
return Err('User must be at least 18 years old')
}
return Ok(createUser(result.value))
}
// Unified handling
const result = validateAndCreate(req.body)
if (result.kind === 'Err') {
return res.status(400).json({ error: result.value })
}
res.json(result.value)Comparison with Other Libraries
vs Zod
Basic Definitions
typescript
// Zod
import { z } from 'zod'
const UserSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email()
})
type User = z.infer<typeof UserSchema>
// farrow-schema
import { ObjectType, String, Number } from 'farrow-schema'
class User extends ObjectType {
name = String
age = Number
email = EmailType // Custom validator
}
type UserType = TypeOf<typeof User>Recursive Types
typescript
// Zod - Need explicit recursive definition
type Category = {
name: string
subcategories: Category[]
}
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema)
})
)
// farrow-schema - Directly supports recursion
class Category extends ObjectType {
name = String
subcategories = List(Category) // ✅ Natural recursive reference
}Feature Comparison Table
| Feature | Zod | farrow-schema |
|---|---|---|
| Definition method | Functional API | Class inheritance |
| Type inference | z.infer<> | TypeOf<> |
| Recursive references | Need z.lazy() | ✅ Native support |
| Optional fields | .optional() | Optional() |
| Nullable fields | .nullable() | Nullable() |
| Data transformation | .transform() | Lenient mode auto-convert |
| Default values | .default() | ❌ Not supported |
| Chain validation | ✅ .min().max() | Need custom ValidatorType |
| Schema operations | .omit() .pick() .partial() | omitObject() pickObject() partialObject() |
| Error messages | Detailed issues array | Single error + path |
| Custom validation | .refine() .superRefine() | Inherit ValidatorType |
| Metadata extraction | ❌ Not supported | ✅ Formatter.format() |
| Bundle size | ~8kb (minified) | ~12kb (minified) |
| Ecosystem | 🌟 Rich (trpc, react-hook-form, etc.) | 🔧 farrow ecosystem |
| Learning curve | Gentle | Medium |
Advantage Comparison
Zod's advantages:
- ✅ More mature ecosystem (integrations with tRPC, React Hook Form, etc.)
- ✅ Chain API, more aligned with functional programming style
- ✅ More built-in convenience methods (
.default(),.transform(), etc.) - ✅ More detailed error messages
- ✅ Better community support
farrow-schema's advantages:
- ✅ More natural recursive references (no
lazy()needed) - ✅ Class inheritance closer to traditional OOP
- ✅ Metadata extraction system (for code generation, documentation generation)
- ✅ Deep integration with farrow ecosystem (farrow-http, farrow-api)
- ✅ More flexible validator extensions (ValidatorType)
Usage Scenario Recommendations
Choose Zod if:
- You need rich third-party integrations (tRPC, React Hook Form, etc.)
- You prefer functional API
- You need frequent data transformation and default values
- Your team is more familiar with Zod
Choose farrow-schema if:
- You use farrow framework (farrow-http, farrow-api)
- You need complex recursive data structures
- You need metadata extraction for code generation
- You prefer class inheritance approach
- You need better interoperability with TypeScript classes
Frequently Asked Questions
Q: Should I use ObjectType or Struct?
A:
- Default to ObjectType (supports recursive references)
- Use Struct only for union type variants and dynamic building
Q: What's the difference between Optional and Nullable?
A:
Optional(T)→T | undefined, field can be omittedNullable(T)→T | null, field must exist but value can be null
typescript
// Optional: field can be omitted
{ name: 'Alice' } // ✅
{ name: 'Alice', bio: undefined } // ✅
// Nullable: field must exist
{ name: 'Alice' } // ❌ content missing
{ name: 'Alice', content: null } // ✅Q: How to handle validation errors?
A: Recommended to use result.kind for type narrowing:
typescript
const result = Validator.validate(User, data)
// Recommended: Use kind field
if (result.kind === 'Err') {
console.log('Error:', result.value.message)
console.log('Path:', result.value.path?.join('.'))
return
}
// result.value is type-safe
console.log(result.value)Q: Can it be used in browsers?
A: Yes! farrow-schema is a pure TypeScript library, supports both browsers and Node.js
Q: How to implement custom validation logic?
A: Inherit ValidatorType:
typescript
class PositiveNumber extends ValidatorType<number> {
validate(input: unknown) {
const result = Validator.validate(Number, input)
if (result.kind === 'Err') return result
if (result.value <= 0) {
return this.Err('Must be positive')
}
return this.Ok(result.value)
}
}Cheat Sheet
Basic Types
typescript
String // String
Number // Number
Int // Integer
Float // Float
Boolean // Boolean
Date // Date
ID // Identifier (non-empty string)
Any // Any type
Unknown // Unknown type
Json // JSON typeComposite Types
typescript
List(String) // string[]
Optional(String) // string | undefined (field can be omitted)
Nullable(String) // string | null (field must exist)
Record(String) // { [key: string]: string }
Tuple(String, Number) // [string, number]
Union(Type1, Type2) // Type1 | Type2
Intersect(Type1, Type2) // Type1 & Type2
Literal('value') // 'value'Schema Operations
typescript
pickObject(Schema, ['field1', 'field2']) // Select fields
omitObject(Schema, ['field1']) // Exclude fields
partialObject(Schema) // All fields optional
requiredObject(Schema) // All fields required
keyofObject(Schema) // Get field keys
Intersect(Schema1, Schema2) // Merge SchemasModifiers
typescript
Strict(Number) // Strict mode validation
NonStrict(Number) // Non-strict mode validation
ReadOnly(Type) // Read-only (shallow)
ReadOnlyDeep(Type) // Read-only (deep)Validators
typescript
import { Validator } from 'farrow-schema/validator'
// Validate data (strict mode by default)
const result = Validator.validate(Schema, data)
// Use kind field for type narrowing (recommended)
if (result.kind === 'Ok') {
console.log(result.value) // Validation successful
} else {
console.log(result.value.message) // Error message
console.log(result.value.path) // Error path
}
// Lenient mode (allows type conversion)
const result = Validator.validate(Schema, data, { strict: false })
// Create dedicated validator (strict mode by default)
import { createSchemaValidator } from 'farrow-schema/validator'
const validate = createSchemaValidator(Schema)
const validateLenient = createSchemaValidator(Schema, { strict: false })Next Steps
Recommended learning path:
- 📖 Check Complete API Reference - Learn all API details
- 🔧 Learn farrow-http Integration - Use in HTTP services
- 🎯 Explore farrow-api code generation - Automatically generate client code
Practice suggestions:
- Try farrow-schema in small projects
- Create reusable validator libraries
- Write best practices documentation for your team
Remember: Good Schema design can make bugs nowhere to hide and make code more type-safe! 🚀
