Signing JavaScript objects with Symbol and Proxy

Matt Miller
3 min readJan 18, 2022

ECMAScript 6 includes powerful new APIs like Proxy() and its helpful companion Reflect. Here I want to explore how we can take advantage of a combination of proxies and symbols to create “secret” mix-ins for your objects!

If you haven’t taken advantage of symbols yet, I recommend reading up on them on the MDN documentation. In short:

  • symbols are primitives
  • they can be used as keys on objects, like strings
  • a symbol can only be used via the reference it was created with — in other words, a symbol is only equal to itself
  • because of the previous, they cannot be serialized to or revived from JSON
  • if you don’t have access to the symbol in your scope, you cannot read it!

We can use symbols to augment existing objects (or spread them) and add properties that we know won’t collide with existing properties on the object. In other words, symbols can be a powerful tool for creating safe “mix-ins” or for “branding” objects.

const entityTypeSym: unique symbol = Symbol();type WithEntityType<
T extends object,
S extends symbol
> = T & {
readonly [entityTypeSym]: S;
};
const USER: unique symbol = Symbol();type WithUserType<T extends object> =
WithEntityType<T, typeof USER>;
function withUserType<T extends object>(
object: T
): WithUserType<T> {
return {
...object,
[entityTypeSym]: USER,
};
}
function isUserType<T>(value: T): value is WithUserType<T> {
return value !== null && typeof value === "object" &&
value[entityTypeSym] === USER;
}
const data = { username: "foo" } as const;
// data: { username: string }
const user = withUserType(data);
// user: { [entityTypeSym]: USER, username: string }

This simple implementation is pretty neat, but it has some side effects — namely, that iterating over the object will enumerate the symbol, and we want our mix-in to be more opaque. We can get around this with Object.defineProperty(), but any property attached to an object is generally made accessible by the internal implementation of Object.getOwnPropertyDescriptors().

// :(
for (const prop in user) {
alert(prop);
// Uncaught TypeError: Cannot convert a Symbol value to a string
}

What if we were able to make our property accessible by symbol, but without it actually being attached to the object at all? Then, if our entityTypeSym key remains private (scoped to our module), then only the functions in our module will be able to read or write its value.

This is actually possible thanks to Proxy()!

function withUserType<T extends object>(
object: T
): WithUserType<T> {
return new Proxy(object, {
get: (target, prop, receiver) => {
if (prop === entityTypeSym) {
return USER;
}
return Reflect.get(target, prop, receiver);
},
}) as WithUserType<T>;
}

The return value of withUserType is a facade object that wraps the given object and actually intercepts the built-in behavior for accessing a property on the object! So while the other proxy functions are not trapped and simply forward onto the object, we catch access for our private entityTypeSym symbol and return its type.

Since the other methods of our proxied object aren’t trapped, functions like Object.getOwnPropertyDescriptors() will never return our symbol — meaning that, outside our module, code can pass around our proxied object and won’t have access to our private data!

However, any functions within our module that do have access to the symbol, can read and write to it, with the confidence that other code could not have manipulated any of the values.

function getType<T>(value: T): symbol | undefined {
return value !== null && typeof value === "object" &&
value[entityTypeSym];
}
getType(user); // Code outside this module can never read
// entityTypeSym nor value[entityTypeSym]
// without our getType() function

What could we use this for?

I have an object. I frequently need to reference the number of keys it has; however, the general way to do this, Object.keys(object).length is a slightly expensive operation, given that it has the overhead of creating an array of all enumerable keys, just to get its size.

What if we could use a proxy to keep track of the number of properties on an object, and a private symbol to allow access to this value?

Pretty neat, right?

Let me know in the comments any other cool applications you’ve come up with for proxies and symbols!

--

--