Solving Yup 'when' Condition in Nested Objects: Accessing Outer Boolean to Fix 'Cannot use 'in' operator' Error
Yup is a powerful JavaScript schema builder for value validation, widely used in form validation (e.g., with React Hook Form or Formik). Its when condition allows you to dynamically adjust validation rules based on other fields’ values. However, when working with nested objects, accessing outer properties (e.g., a root-level boolean) in a nested when condition often triggers the frustrating error: "Cannot use 'in' operator to search for 'outerField' in undefined".
This blog demystifies why this error occurs and provides a step-by-step solution to correctly reference outer properties in nested when conditions. We’ll use a real-world example, break down common pitfalls, and explore alternative approaches to ensure your validation logic works seamlessly.
Table of Contents#
- Understanding the "Cannot use 'in' operator" Error
- The Yup 'when' Condition: A Quick Recap
- Scenario: Nested Object with Outer Boolean Dependency
- Common Pitfalls: Why Accessing Outer Properties Fails in Nested 'when'
- Step-by-Step Solution: Correctly Reference Outer Properties
- Alternative Approaches
- Testing the Solution
- Conclusion
- References
Understanding the "Cannot use 'in' operator" Error#
Before diving into solutions, let’s decode the error message:
"Cannot use 'in' operator to search for 'X' in undefined"
This error occurs when Yup tries to check if a property (e.g., X) exists in an object, but the object itself is undefined. In the context of nested when conditions, it typically means Yup is unable to find the outer property you’re referencing (e.g., isEmployee) because it’s looking in the wrong context (the nested object instead of the root).
The Yup 'when' Condition: A Quick Recap#
The when method lets you conditionally apply validation rules based on the value of another field. Its basic syntax is:
yup.object().shape({
fieldA: yup.boolean(),
fieldB: yup.string().when('fieldA', {
is: true,
then: (schema) => schema.required('Field B is required when Field A is true'),
otherwise: (schema) => schema.optional(),
}),
});Here, fieldB’s validation depends on fieldA. The when condition takes the dependent field name (fieldA), an object with is (the condition), then (schema if condition is met), and otherwise (schema if not).
Scenario: Nested Object with Outer Boolean Dependency#
Let’s define a practical scenario to illustrate the problem. Suppose we’re building a user registration form with:
- A root-level boolean
isEmployee(true if the user is an employee). - A nested object
employeeDetails(with fields likeidanddepartment) that should be required only ifisEmployeeis true.
Initial (Faulty) Schema#
Here’s an attempt to write this schema, which will trigger the "Cannot use 'in' operator" error:
import * as Yup from 'yup';
const userSchema = Yup.object().shape({
isEmployee: Yup.boolean().required('isEmployee is required'),
employeeDetails: Yup.object().shape({
id: Yup.string().required('Employee ID is required'),
department: Yup.string().required('Department is required'),
}).when('isEmployee', { // ❌ Error occurs here
is: true,
then: (schema) => schema.required('Employee details are required for employees'),
otherwise: (schema) => schema.optional(),
}),
});Why This Fails#
When employeeDetails.when('isEmployee') runs, Yup looks for isEmployee within the employeeDetails object (not the root). Since employeeDetails doesn’t have an isEmployee property, Yup throws: "Cannot use 'in' operator to search for 'isEmployee' in undefined".
Common Pitfalls When Accessing Outer Properties#
To fix the error, we first need to understand why nested when conditions struggle with outer properties:
1. Incorrect Context#
Nested schemas (like employeeDetails) have a context limited to their parent object. By default, when('field') searches for field in the current context (the nested object), not the root.
2. Missing Path Specification#
Yup requires explicit paths to reference properties outside the current context. For example, to access the root isEmployee from employeeDetails, you need to specify its full path (e.g., '../isEmployee' or 'isEmployee' with a resolver).
3. Misusing this#
Developers sometimes try when(this.isEmployee), but this in the nested schema refers to the employeeDetails object, not the root.
Step-by-Step Solution: Access Outer Properties in Nested 'when'#
The fix involves telling Yup to explicitly reference the outer property using its full path and ensuring the when condition resolves the root context. Here’s how:
Step 1: Use the Full Path to the Outer Property#
Yup allows referencing parent properties using relative or absolute paths. For root-level properties, use the absolute path (e.g., 'isEmployee' with a resolver) or the resolve function to access the root object.
Step 2: Use Yup.lazy or when with a Resolver#
For nested when conditions, use Yup.when with a dependency array and a resolver function to access the root value. Here’s the corrected schema:
Corrected Schema#
import * as Yup from 'yup';
const userSchema = Yup.object().shape({
isEmployee: Yup.boolean().required('isEmployee is required'),
employeeDetails: Yup.object().shape({
id: Yup.string().required('Employee ID is required'),
department: Yup.string().required('Department is required'),
}).when(
// Step 1: Specify the outer property as a dependency (root-level 'isEmployee')
['isEmployee'],
// Step 2: Resolver function to access the root value
(isEmployee, schema) => {
return isEmployee
? schema.required('Employee details are required for employees')
: schema.optional();
}
),
});Why This Works#
- Dependency Array:
['isEmployee']tells Yup to watch the root-levelisEmployeeproperty (not the nested one). - Resolver Function: The resolver receives the value of
isEmployee(from the root) and the currentschema(foremployeeDetails). It dynamically returns the required/optional schema based onisEmployee.
Testing the Resolver#
To confirm Yup is accessing the root isEmployee, log the value in the resolver:
.when(['isEmployee'], (isEmployee, schema) => {
console.log('Root isEmployee value:', isEmployee); // Logs true/false from root
return isEmployee ? schema.required() : schema.optional();
});Alternative Approaches#
If the resolver method feels verbose, consider these alternatives:
Approach 1: Root-Level 'when'#
Move the conditional logic to the root schema, using when to dynamically add the employeeDetails schema:
const userSchema = Yup.object().shape({
isEmployee: Yup.boolean().required('isEmployee is required'),
}).when('isEmployee', {
is: true,
then: (schema) =>
schema.shape({
employeeDetails: Yup.object().shape({
id: Yup.string().required('Employee ID is required'),
department: Yup.string().required('Department is required'),
}).required('Employee details are required'),
}),
otherwise: (schema) =>
schema.shape({
employeeDetails: Yup.object().optional(),
}),
});Pros: Simplifies pathing (no nested context issues).
Cons: Less modular for complex nested objects.
Approach 2: Use Yup.lazy#
Yup.lazy defers schema creation until validation, allowing access to the root value:
const userSchema = Yup.object().shape({
isEmployee: Yup.boolean().required('isEmployee is required'),
employeeDetails: Yup.lazy((value, context) => {
const { isEmployee } = context.parent; // Access root via context.parent
return isEmployee
? Yup.object().shape({
id: Yup.string().required('Employee ID is required'),
department: Yup.string().required('Department is required'),
}).required('Employee details are required')
: Yup.object().optional();
}),
});Pros: Flexible for dynamic schema generation.
Cons: Requires understanding Yup’s context object.
Testing the Solution#
Validate the schema with sample data to ensure it works:
Valid Case (isEmployee: true)#
const validData = {
isEmployee: true,
employeeDetails: { id: '123', department: 'Engineering' },
};
userSchema.validate(validData)
.then(() => console.log('Valid!'))
.catch((err) => console.error('Error:', err.errors));
// Output: "Valid!"Invalid Case (isEmployee: true, missing employeeDetails)#
const invalidData = { isEmployee: true };
userSchema.validate(invalidData)
.catch((err) => console.error('Error:', err.errors));
// Output: "Error: ["Employee details are required for employees"]"Valid Case (isEmployee: false)#
const validData = { isEmployee: false };
userSchema.validate(validData)
.then(() => console.log('Valid!'));
// Output: "Valid!" (employeeDetails is optional)Conclusion#
The "Cannot use 'in' operator" error in Yup nested when conditions arises from misaligned context—Yup searches for the outer property in the nested object instead of the root. The solution is to:
- Explicitly reference the outer property using its full path or dependency array.
- Use a resolver function to access the root value in the
whencondition.
Alternatives like root-level when or Yup.lazy offer flexibility, but the resolver method is most direct for nested scenarios. By following these steps, you’ll ensure your Yup schemas dynamically validate nested objects based on outer properties without errors.