DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Proxy and Reflect in JavaScript

logotech

## Unveiling Proxies and Reflect in JavaScript: The Power to Intercept and Validate Objects

In the world of backend development, especially with Node.js, we often encounter the need to add custom behaviors to existing objects, validate their access, or even intercept operations for logging or security purposes. Imagine a scenario where you want to ensure that a configuration object is never modified after its initialization, or that a user object always has a specific format before being saved to the database. How can we achieve this flexibility and control without cluttering our code with repetitive logic?

The answer lies in two powerful features of modern JavaScript: Proxies and the Reflect object. Together, they allow us to create intelligent \"wrappers\" for our objects, intercepting and manipulating operations like reading, writing, deleting properties, and more.

The Problem: Rigid Objects and Manual Validation

Traditionally, to add validation or custom behaviors, we would resort to:

  1. Getters and Setters: Useful for individual properties, but can become verbose and difficult to manage in complex objects with many properties.
  2. Validation/Wrapper Functions: Creating separate functions that validate or modify an object before using it. This can lead to code duplication and difficulty in ensuring validation is always applied.

These approaches, while functional, can make code less readable and more error-prone.

The Solution: Proxies and Reflect

1. Proxies: The Intelligent Interceptor

A Proxy in JavaScript is an object that wraps another object (the \"target\") and allows you to intercept fundamental operations (like property lookup, assignment, enumeration, function invocation, etc.) applied to the target. It does this through a \"handler\" — an object whose properties are functions (called \"traps\") that define custom behavior for specific operations.

The basic syntax is:

const handler = {
  get(target, prop, receiver) {
    // Logic to intercept reading the 'prop' property
    return Reflect.get(target, prop, receiver); // Default operation
  },
  set(target, prop, value, receiver) {
    // Logic to intercept writing the 'prop' property
    return Reflect.set(target, prop, value, receiver); // Default operation
  }
  // ... other traps
};

const target = { name: \"Example\" };
const proxy = new Proxy(target, handler);
Enter fullscreen mode Exit fullscreen mode
  • target: The original object we want to \"wrap.\"
  • handler: An object containing the \"traps\" (functions) to intercept operations.
  • prop: The name of the property being accessed or modified.
  • value: The new value to be assigned (in the set trap).
  • receiver: The proxy or inheritance object being used for the operation.

2. Reflect: The Bridge to Default Operations

The Reflect object provides methods that correspond to the fundamental operations that proxies can intercept. It's crucial because, within the handler's traps, we need a way to delegate the actual operation to the target object. Using Reflect ensures we do this consistently and correctly, especially in inheritance scenarios.

For example, instead of writing target[prop] = value within the set trap, we use Reflect.set(target, prop, value, receiver). This ensures that if target has custom setters or if we are dealing with prototypes, the behavior is as expected.

Development: Creating a Validation Proxy

Let's build a practical example: a proxy that validates if properties of a User object are assigned correctly and prevents modification of certain properties after creation.

/**
 * Interface to represent a user.
 */
interface User {
  id: number;
  name: string;
  email: string;
  readonly isAdmin?: boolean; // Read-only property
}

/**
 * Handler for the Proxy that adds validation and mutability control.
 */
const validationHandler = {
  /**
   * Trap to intercept property assignment.
   * @param target - The target object.
   * @param prop - The name of the property to set.
   * @param value - The value to assign to the property.
   * @param receiver - The proxy or inheritance object.
   * @returns true if the assignment was successful, false otherwise.
   */
  set(target: User, prop: keyof User, value: any, receiver: any): boolean {
    console.log(`Attempting to set property \"${String(prop)}\" with value:`, value);

    // Email validation (simple example)
    if (prop === 'email' && typeof value === 'string' && !value.includes('@')) {
      console.error(`Error: Invalid email \"${value}\". Please provide a valid email.`);
      return false; // Prevent assignment
    }

    // Prevent modification of 'readonly' properties
    if (Object.prototype.hasOwnProperty.call(target, prop) && Object.getOwnPropertyDescriptor(target, prop)?.writable === false) {
       console.error(`Error: Property \"${String(prop)}\" is read-only and cannot be modified.`);
       return false; // Prevent assignment
    }

    // If all validations pass, use Reflect.set to assign the value to the target object.
    // Using Reflect.set ensures that the default behavior (and any custom logic
    // on the target object, like setters) is executed correctly.
    const success = Reflect.set(target, prop, value, receiver);

    if (success) {
      console.log(`Property \"${String(prop)}\" successfully set to:`, value);
    } else {
      console.error(`Failed to set property \"${String(prop)}\".`);
    }

    return success;
  },

  /**
   * Trap to intercept property reading.
   * @param target - The target object.
   * @param prop - The name of the property to get.
   * @param receiver - The proxy or inheritance object.
   * @returns The value of the property.
   */
  get(target: User, prop: keyof User, receiver: any): any {
    console.log(`Accessing property \"${String(prop)}\"`);
    // Use Reflect.get to get the property value from the target object.
    // This ensures default behavior and respects getters on the target object.
    return Reflect.get(target, prop, receiver);
  },

  /**
   * Trap to intercept property deletion.
   * In this example, we will disallow property deletion.
   * @param target - The target object.
   * @param prop - The name of the property to delete.
   * @returns true if the deletion was successful (or allowed), false otherwise.
   */
  deleteProperty(target: User, prop: keyof User): boolean {
    console.error(`Error: Deleting property \"${String(prop)}\" is not allowed.`);
    return false; // Prevent deletion
  }
};

// --- Usage Example ---

// Initial target object
const userProfile: User = {
  id: 1,
  name: \"Alice\",
  email: \"alice@example.com\",
  isAdmin: true // Read-only property
};

// Creating the proxy with the validation handler
const securedUserProfile = new Proxy(userProfile, validationHandler);

// Testing the operations
console.log(\"\n--- Testing the Proxy ---\");

// Reading a property
console.log(\"User name:\", securedUserProfile.name);

// Attempting to set a valid property
securedUserProfile.name = \"Alice Smith\"; // Should pass

// Attempting to set an invalid email
securedUserProfile.email = \"alice-invalid-email\"; // Should fail

// Attempting to set a valid email
securedUserProfile.email = \"alice.smith@example.com\"; // Should pass

// Attempting to modify a 'readonly' property
try {
  // @ts-ignore // Ignoring typing error for demonstration
  securedUserProfile.isAdmin = false; // Should fail
} catch (e) {
  console.error(\"Caught error when trying to modify 'isAdmin':\", e);
}

// Attempting to delete a property
try {
  // @ts-ignore
  delete securedUserProfile.name; // Should fail
} catch (e) {
  console.error(\"Caught error when trying to delete 'name':\", e);
}

console.log(\"\nFinal user profile:\", securedUserProfile);

// Checking if the original object was modified (yes, the proxy acts on it)
console.log(\"Original profile (userProfile):\", userProfile);
Enter fullscreen mode Exit fullscreen mode

Code Explanation:

  1. User Interface: Defines the expected structure for our user objects, including a readonly property to demonstrate mutability control.
  2. validationHandler:
    • set(target, prop, value, receiver): This is the primary \"trap.\" It's triggered every time we try to assign a value to a proxy property.
      • First, we log the attempt.
      • We implement a simple validation for email. If the format is invalid, we return false, which prevents the assignment and signals an error.
      • We check if the property is readonly using Object.getOwnPropertyDescriptor. If it is, we prevent modification.
      • If validations pass, we use Reflect.set(target, prop, value, receiver) to actually set the property on the target object. Reflect.set is the correct way to perform the default operation.
      • We return true or false to indicate success or failure of the operation.
    • get(target, prop, receiver): Intercepts property reading. Here, we simply use Reflect.get to get the value from the target. We could add logging or transform values here.
    • deleteProperty(target, prop): Intercepts property deletion. In this example, we explicitly disallow deletion by returning false.
  3. Usage Example: Demonstrates how to create the original userProfile, wrap it in a Proxy using the validationHandler, and then test read, write (valid and invalid), and delete operations.

Conclusion

Proxies and Reflect are extremely powerful tools for creating more robust, secure, and expressive code in Node.js and the frontend. They allow us to:

  • Validate input data: Ensure objects maintain a consistent state.
  • Control mutability: Create \"immutable\" objects or those with read-only parts.
  • Add logging and observability: Monitor access and modifications to critical objects.
  • Implement design patterns: Such as \"Lazy Loading\" or \"Lazy Initialization."
  • Mock dependencies: In unit tests, Proxies are essential for simulating object behaviors.

By mastering these tools, you elevate your ability to handle complexity and build more elegant and resilient backend applications. Remember to use Reflect within your handler's traps to ensure default operations are performed correctly, especially in more complex scenarios.

Top comments (0)