Skip to content

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 information

4. 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 fields

5. 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

FeatureZodfarrow-schema
Definition methodFunctional APIClass inheritance
Type inferencez.infer<>TypeOf<>
Recursive referencesNeed 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 messagesDetailed issues arraySingle error + path
Custom validation.refine() .superRefine()Inherit ValidatorType
Metadata extraction❌ Not supportedFormatter.format()
Bundle size~8kb (minified)~12kb (minified)
Ecosystem🌟 Rich (trpc, react-hook-form, etc.)🔧 farrow ecosystem
Learning curveGentleMedium

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 omitted
  • Nullable(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 type

Composite 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 Schemas

Modifiers

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:

  1. 📖 Check Complete API Reference - Learn all API details
  2. 🔧 Learn farrow-http Integration - Use in HTTP services
  3. 🎯 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! 🚀

This is a third-party Farrow documentation site | Built with ❤️ and TypeScript