Chain Order: The #1 Gotcha in CtroEnv

2026-06-20·Ctrotech·
guidedebugging

Chain Order: The #1 Gotcha in CtroEnv

Every validator starts with type-specific methods like .url() or .min(), then loses them after calling .secret() or .optional(). Get the order wrong and you get a TypeScript error that's easy to fix once you understand why.

The Problem

This looks reasonable but fails:

string().secret().url()
//          ~~~~~~~
// Property 'url' does not exist on type 'Validator<string> & ChainableMethods<string>'

The fix is to swap the order:

string().url().secret()  // ✅

Why It Happens

Each chainable method returns a generic Validator & ChainableMethods wrapper. The type-specific methods (.url(), .email(), .min(), .int(), .port()) only exist on StringValidator and NumberValidator — not on the generic wrapper.

The call chain:

string()              → StringValidator  (has .url, .email, .port, .min, .max, .regex)
  .url()              → StringValidator  (still has all methods)
  .secret()           → Validator & ChainableMethods  (lost .url, .email, etc.)
  .min(1)             ❌ doesn't exist

Correct Order

Type-specific refinements first, then chainable methods:

// ✅ String
string().url().min(1).secret()
string().email().optional()
string().port().describe("Redis port")
string().hostname().regex(/^myapp-/).secret()

// ✅ Number
number().int().positive().min(1).max(100).default(50)
number().port().optional()
number().min(0).describe("Timeout in ms")

// ✅ Chainable methods can be in any order among themselves
string().url().secret().describe("API URL").optional()
//                                          ^^^^^^^^
// Wait — .optional() after .secret() is fine because both return
// the same generic wrapper. But .optional() at the end means
// the value can be undefined. That's intentional.

What About .describe() and .validate()?

.describe() and .validate() are also chainable methods — they return the generic wrapper. But they don't conflict because you'd never need type-specific methods after describing:

string().url().describe("API URL")  // ✅ fine — no .url() needed after describe

Debugging Chain Order Errors

TypeScript error:

Property 'url' does not exist on type 'Validator<string> & ChainableMethods<string>'

This means you called a chainable method (.secret(), .optional(), .describe(), or .validate()) before a type-specific method. Move the type-specific call before the chainable one.

Why This Design?

CtroEnv keeps the core package at zero dependencies and 4 KB gzipped. A builder-pattern chain that preserved all type methods would require complex generic machinery or runtime type tracking. The current design is explicit and predictable: type-specific refinements first, modifiers second.

Quick Reference

// ✅ Correct
string().min(1).max(255).url().secret()
number().int().positive().default(42)
pick(["a", "b"]).optional()
boolean().default(false)

// ❌ Wrong
string().secret().url()
number().optional().int()
pick(["a", "b"]).secret()  // works (pick has no type refinements to lose)

pick() has no type-specific refinements, so chain order doesn't matter for it. Same with boolean(), semver(), ip(), uuid(), and guid().