You must or must not have heard of object proxies before, but have you ever understood how to use them? What the actual usecase of an object proxy is? In this talk, we will do a deep dive into object proxies, understanding how they work, and the kinds of things you can build using them. We will build higher-order wrappers for existing libraries, and learn how to wrap over them in a type-safe manner.
Unleashing Object Proxies: Building Type-Safe Wrappers for Anything
This talk has been presented at TypeScript Congress 2023, check out the latest edition of this JavaScript Conference.
FAQ
Object proxies serve as middleware for objects, acting as a wrapper to control both the input received by the object and the output returned when a user accesses any method or property of the object.
Object proxies can be used for input validation and implementing an access control layer. For instance, they can manage and restrict access to sensitive data within an object based on the user's or developer's permissions.
Object proxies are commonly used for controlling data access, adding logging to track and monitor access to sensitive information, and handling responses dynamically, such as lazy loading data only when it is accessed.
Prisma uses object proxies to manage database interactions. It generates types for database tables and applies them to an object proxy, which then handles real-time data fetching and operations transparently as if accessing local properties.
Object proxies can automatically log access attempts to objects, especially unauthorized ones. This helps in monitoring and auditing access to sensitive data or operations within an application.
In response handling, object proxies can manipulate the return values based on the input or conditions, such as performing API calls for data fetching when a property is accessed, thereby enabling dynamic data loading and caching mechanisms.
TypeScript can be used to define and ensure the type safety of the properties handled by object proxies. It helps in generating types that match the expected structure of the data managed by the proxies, enhancing reliability and developer experience.
Object proxies enhance security and data integrity by controlling access and validation, improve performance through lazy loading and caching, and offer flexibility in handling data interactions and transformations within applications.
1. Introduction to Object Proxies#
I'm going to talk about unleashing object proxies, building type-safe wrappers for anything. Object proxies are middleware for your objects that allow you to control input and output. They have various use cases such as controlling data access, adding logging, and handling responses. Prisma and PRPC are examples of frameworks that use object proxies to access databases and enable transparent method calls.
Hey guys, my name is Akash Joshi. I'm a software engineer at SIGTECH and today I'm going to talk about unleashing object proxies, building type-safe wrappers for anything. Object proxies are an amazing tool that are severely under-explored but are quite widely used in all of the libraries that we use in our day-to-day. So I'm going to explain how they work, what they are and how you can use them as well.
So what are object proxies? Object proxies are basically middleware for your objects. So think of them as a wrapper around your object with which you can control what input comes into the object and what you return from the object whenever a user tries to access a method or a property. So what are some of the use cases that we see with object proxies in the real life? So one of the first use cases that one can think of is controlling data access. So doing input validation or implementing an access control layer on any sort of object which can contain privileged properties. So for example, on a bank, if you want to create an object which contains the user's data then based on whether a user has, one of your developers have, access to that property or not, you can actually do input validation on it. So if the user or the developer has access to that property, then you return it, otherwise you can return an error or whatever you want to do instead.
Similarly, you can add logging to your objects via object proxies. So if a user tries to repeatedly access an unauthorized data, or you just want to have a log of all of the activities that your developers are doing on any privileged object, then you can use, you can add logging to it by object proxies. Any accesses are logged to one of your access logs. But most importantly, what we use object proxies for is response handling. So this can involve returning something else based on what the user provides or lazy loading certain objects. So for example, just continuing with the user case. Let's say we have an object which contains a user's banking details, like their address history, their transaction history, anything. But you don't want to fetch all of it at runtime. So whenever someone tries to access their address history, then we make a network call to the address API, get all of that data, and then return it via the output. So on the on the developer side, it just looks like you made a property call, like a .call to an object. So that would be user.address. But in the backend, it will actually make an API call. And you can also implement caching and other things on top of it to actually make your object calls faster.
In the wild, object proxies are used by Prisma. So have you ever thought about how Prisma knows what all of your tables are and how it is able to access all of them at runtime, even though it actually isn't present in their codebase? What they do is they generate the types for all of your tables and then they apply them to an object proxy. So whenever you are accessing your property by using the dot method, so like prisma.analytics, prisma.user, prisma.posts, what it actually does is it makes a network call via object proxies to the actual database and then fetches those results and then returns them to your client. Of course, it isn't all hard-coded into the Prisma's codebase, but they generate it at runtime via TypeScript and then object proxies take care of the rest. Similarly for PRPC, PRPC works on a similar concept where they allow you to make object calls, make method calls, from your front end to your back end quite transparently. They use object proxies as well. They expose a router object on the back end and expose its types and then use those types on the front end.
2. Implementing Object Proxies#
The actual proxying happens on the front end where any call to one of their queries is transpiled by the object proxy into a network call. We are going to implement a simple version of the TRPC library and explain how object proxies work. On the server, we have a Fastify server with a route that accepts a procedure name and parameters, applies them to the procedure, and returns the response. The API object provided by the developer contains the exposed APIs. On the client side, we define an object proxy to access properties and make method calls. We use the get, has, and apply properties of the proxy to handle property access and method calls.
So the actual proxying happens on the front end where any call to one of their queries is actually transpiled by the object proxy into a network call and then the network call executes on the back end to actually return you the result on the front end. So what we are going to do now is we are going to implement a very simple version of the TRPC library or just try to implement a little bit of RPC to the back end ourselves using object proxies. And I'm going to also explain how object proxies work.
This just involves a little bit of code. Firstly, let's look at what our server looks like. So this is a very simple Fastify server. If you go to Fastify's documentation page, this is similar to what you will see on the main page. In fact, this is what I've copied from there. The only route that I've added here is slash RPC. It accepts a procedure name and a set of parameters for that procedure. And it then parses this to this data to string and then it applies those parameters on those procedure and then returns the response to the front end.
Where are we getting the actual procedure from? Let's say we have an API object which the developer provides to the server and this API object contains all of the APIs that the developer actually wants to expose to the client. In this case, we are exposing hello, which just returns world and then another method called sum, which returns the summation of two numbers. And see how we have actually defined the types correctly here. This is because we are going to use the properties of TypeScript to actually pass that type to the client as well.
On the client side now what we are going to do is that we are going to define a very simple object proxy. Have a look at the main method here where I will define a few lines of code which try to access some properties of the object proxy and try to get the results of it. So the first one is that we are trying to destruct a property called hello on an object that doesn't exist yet. Next we are checking for two properties whether they exist on the object and lastly we are trying to make method calls on the object proxy and then console.logging the result of it. So let's quickly define our object proxy here. The syntax for defining object proxies is to use a new proxy to define them. The first parameter it takes is the target object that you want to build it upon. So the standard way would be to use a wrapper over an existing object. In this case we are just going to use the properties of the wrapper so we don't use any existing object so I pass in an empty object.
Next we actually look at all of the properties that a proxy accepts. So they are primarily get, has and apply. These are the properties that you will be mostly looking at while building object proxies. The get property is the dot method so when you are destructing hello or you are doing client proxy dot some or client proxy dot anything else then the get method is called and you can return whatever you want from it. Has method is sort of like has own property which actually checks for whether a property exists in object proxy or not and in this case we are just returning true so these two methods which check whether these two parameters exist in the object property should actually return true even though they don't really exist there and lastly we have apply I don't think we have enough time to go into the depth of it but it is a trap for any method calls that you make on your object proxy. In this case get should be enough we will go into the depth of it as we actually define it.
3. Handling Object Proxy Traps and Errors#
We have a type error because the property hello does not exist on the empty object. We need to fake the object's structure. By logging the target object and the property being called, we can see the actual output. The error hello is not a function is expected as we haven't defined the code for it. We are interested in the first three lines, which show the property access and the resulting values. By changing the defined value to false, we can print whatever we want. To resolve the error, we should log the args instead of making a function call.
Let me yeah. So we have right now we have a type error here which says property hello does not exist on type empty object that is because it's actually returning a type of empty object as that is the default object that we passed here. But since we are going to create traps on this object proxy we have to now fake what it actually looks like. So let's try to console log on get here and the first parameter that it takes is the target object. In this case our target is an empty object so we just get underscore we don't really need to use that. What we are actually looking for is the property being called here. And then we will console log on property. And when we run this code here, this executes main which executes all of the lines here. We will see the actual output. So we the error that we are getting here is hello is not a function which is expected because it is calling the hello method here. We are going to define the code for that. But what we are more interested in is the first three lines here we are getting property when we are trying to console log. Sorry, when we are trying to access the client proxy property here and then we are going getting true and true for the property. We are getting true there because we have just defined true here if we change it to false and then we save it. This should run again. So that should then print false that just print whatever we want it to. And then this is actually printing the actual property that we are trying to access which is hello in this case. And yeah let's try to solve this error next. Hello is not a function. So this is because we are trying to make a function method call on the properties being written here. So to resolve this quickly we should just console.log args instead. And if we yeah now it returns correctly and it just prints the args being passed here the property and similar for some. It's printing what method we were calling which is some and then the parameters being passed in here which is 3 and 123.
4. Making API Calls and Defining the Correct Type#
Now we are going to make an API call to our backend and return the data. We use fetch to make the call and pass the parameters correctly. When accessing properties via the calls, it calls the API on the backend. We need to define the correct type by importing the API type from the backend and applying it to the client proxy. The client proxy returns a promise when making a method call. We use the promiseify type to wrap the methods and ensure they return promises. The key takeaways for building ObjectProxies are to embrace, experiment, and explore where they can be found in the real world.
Now we are going to quickly try to make an API call to our back end and return the data based on this what our back end looks like is it accepts a procedure named parameter and a params parameter and based on the procedure name and params that makes the API call. So for hello and params being an empty array it returns world for procedure name being some and the parameters as one and two it returns three and hypothetically for other params it should return the right result as well. So let's quickly do that.
In this case what we do is we just make a fetch call here so fetch to the back end http host 3000. We need to pass in the parameters correctly as well. So I'll just make note of that add async in here. So our procedure name we need url search params. So search params is equal to new URL search params and then add in a procedure name which should be the property and then I believe the args here, params, sorry. So pass in the params as json.stringify args. I think this needs to be a toString and yeah, that resolves that and then we just add this in here into the API call. And then while returning the data we need to do response.json of course. That should return our result. So now when we are running the method again, it should return the result but I believe I am not logging here. So let me have a quick look at what went wrong. So yeah, as we can see here now, when we are trying to access these properties via the calls, this is actually calling the API on the backend. So on the server side, what is happening is that it's accepting the procedure name and params that we are passing in via our URL search params here and then it's parsing them and then returning the actual response via procedure.apply. Now in this case, yeah, that's pretty much it in terms of our object proxy but we are actually missing the actual part still which is actually defining the correct type. So I can actually import the type of API from the backend and then try to apply it to our client proxy here. So let's try to do that. If I do a type of API here, then it should hypothetically apply the correct type to my client proxy here and then if I try to do something like client proxy dot then it should also return the correct type here. But what we are missing is that it doesn't return a promise. In our case, the client proxy actually returns a promise whenever you are making a method call because it is actually making a fetch request. So we define a simple type here called promiseify which extends a record and then, based on the keys for that record, it just applies a promise to the return types of all of the existing methods. Now, in this way, if we wrap our method in this generic that we have just defined, then what we get instead when we are trying to do clientProxy. is that all of the results actually return a promise instead and so we get the correct type on this. And so we don't confuse our users anymore. So that is it for our ObjectProxy. And what are the takeaways? The takeaways here for building your own ObjectProxies are embrace, experiment, and explore. Firstly, embrace ObjectProxies. Try to see, try to find out where you can find ObjectProxies in the real world.
5. Conclusion and Thanks#
Most ORMs and RPC libraries use ObjectProxies. Experiment, create your own RPC libraries, and share them. Explore the edges of ObjectProxies to push their limits. Thank you for being a good audience and happy coding.
You might be surprised to find that most ORMs might be using ObjectProxies and then a lot of RPC libraries use them as well. Experiment with ObjectProxies. Try to create your own RPC libraries. Share them on the internet and just try to have fun with them.
And then finally, explore. Try to push the edges of ObjectProxies. TRPC or Rocket RPC my library wouldn't have been created if you, the people behind them wouldn't have tried to explore the edges of ObjectProxies and what you can do with them. Same for Prisma and a lot of other libraries out there.
So yeah, thank you. Thanks for this talk. Thanks for being a good audience. And happy coding.
Check out more articles and videos
We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career
Workshops on related topic
You will learn:- - What are conditional types and infer notation- What are template strings- How to map over union types and object/array properties.
In this three-hour workshop we’ll introduce React Testing Library along with a mental model for how to think about designing your component tests. This mental model will help you see how to test each bit of logic, whether or not to mock dependencies, and will help improve the design of your components. You’ll walk away with the tools, techniques, and principles you need to implement low-cost, high-value component tests.
Table of contents- The different kinds of React application tests, and where component tests fit in- A mental model for thinking about the inputs and outputs of the components you test- Options for selecting DOM elements to verify and interact with them- The value of mocks and why they shouldn’t be avoided- The challenges with asynchrony in RTL tests and how to handle them
Prerequisites- Familiarity with building applications with React- Basic experience writing automated tests with Jest or another unit testing framework- You do not need any experience with React Testing Library- Machine setup: Node LTS, Yarn
Points Covered:
1. Introduction to JavaScript Patterns2. Foundational Patterns3. Object Creation Patterns4. Behavioral Patterns5. Architectural Patterns6. Hands-On Exercises and Case Studies
How It Will Help Developers:
- Gain a deep understanding of JavaScript patterns and their applications in real-world scenarios- Learn best practices for organizing code, solving common challenges, and designing scalable architectures- Enhance problem-solving skills and code readability- Improve collaboration and communication within development teams- Accelerate career growth and opportunities for advancement in the software industry
In this workshop, we’ll discuss all these finer points while going through a general example of building a code editor using CodeMirror in React. All while sharing some of the nuances our team learned about using this library and some problems we encountered.
Comments