Master TypeScript Proxy traps and the ProxyHandler interface to stop "ghost properties" and enforce strict runtime object integrity in your applications.
Last month, I spent about three hours debugging a production issue where a user object suddenly gained a role_id property that didn't exist in our interface. The code was perfectly typed, but at runtime, a rogue utility function was mutating the object, leaving us with "ghost properties" that bypassed our static checks.
That’s when I turned to the TypeScript Proxy API. While static types are great for development, they vanish the moment your code hits the browser or Node.js. If you want true runtime validation, you need to intercept object operations as they happen.
At its core, a Proxy sits between your code and the target object, acting as a gatekeeper. By implementing a ProxyHandler, you can trap operations like get, set, and deleteProperty.
Here is a basic implementation to prevent unauthorized property additions:
TYPESCRIPTtype User = { id: number; name: string }; function createSecureUser(target: User): User { const handler: ProxyHandler<User> = { set(target, prop, value, receiver) { if (!(prop in target)) { throw new Error(CE9178">`Ghost Property Detected: Cannot add '${String(prop)}' to User object.`); } return Reflect.set(target, prop, value, receiver); } }; return new Proxy(target, handler); }
When you try to assign user.role_id = 123, the set trap fires. Instead of letting the object grow silently, it throws an error immediately. This is the definition of defensive programming.
We often rely on TypeScript Zod Schema Validation: A Guide to Runtime Type Safety for data coming from APIs, but internal state mutation is a different beast. Even if you use TypeScript satisfies operator: Enforce API Contract Integrity to keep your objects clean, a for...in loop or a loose helper function can easily introduce side effects.
When I first attempted this, I tried using Object.freeze(). It failed because our state management logic required shallow updates. The ProxyHandler approach was the only way to allow mutations while strictly forbidding the creation of new keys.
To ensure object integrity across a larger system, you can wrap your state objects in a factory function. This ensures that no matter where the object is passed, the proxy follows it.
TYPESCRIPTfunction enforceIntegrity<T extends object>(target: T): T { return new Proxy(target, { set(target, prop, value, receiver) { if (!Object.prototype.hasOwnProperty.call(target, prop)) { console.warn(CE9178">`Attempted to add property: ${String(prop)}`); return false; // Silently fails in non-strict mode, throws in strict } return Reflect.set(target, prop, value, receiver); } }); }
Using Reflect.set is crucial here. It returns a boolean success value, which is cleaner than manual assignment. It also ensures the internal [[Set]] operation is handled correctly by the engine.
Is this overkill? Sometimes. Proxying every single object in your application will introduce a minor performance hit—roughly 5-10% overhead on property access in some benchmarks I've seen.
However, compare that to the cost of an "undefined is not a function" error deep in your render cycle. For critical configuration objects, I’ve found this pattern invaluable. If you're dealing with dynamic keys, consider looking at TypeScript Proxy API: Building Type-Safe Dynamic Object Access to see how you can handle more complex scenarios without losing your mind.
Q: Does this work with nested objects?
A: No. A standard Proxy is shallow. If you need deep integrity, you’ll need a recursive wrapper that proxies nested objects as they are accessed.
Q: Can I use this with classes?
A: You can, but be careful with this binding. Proxies can break class methods if the this context isn't preserved. Always use bind or Reflect to maintain the correct scope.
Q: Is this slower than standard objects? A: Yes. It’s slightly slower because of the trap overhead. Don't use this inside a hot loop (like a game loop or high-frequency data parser).
I’m still experimenting with using these proxies in conjunction with WeakMap to associate metadata with objects without modifying them. It’s a powerful pattern, but it requires discipline. Start by applying it to your most "fragile" objects—the ones that keep changing structure for no apparent reason—and see if your bug reports drop.
Master TypeScript Proxy API patterns to enforce type-safe dynamic object access. Learn how to eliminate runtime property errors in your API wrappers today.