Type Safety in JavaScript using ES6 Proxies

Currently, most of the solutions for dealing with the lack of type safety in JavaScript are based on static type checking and type inference. TypeScript and Flow extend JavaScript by adding static type annotations, and can validate your code at compile time and use the same Abstract Syntax Tree to output the final JS code. This works very well to improve the developer experience, as the IDE can rely on static analysis to provide autocompletion and developmental aids. Yet, regarding type safety, there is still one major flaw: once compiled in JavaScript and run in the browser, there is no longer any guarantee that the variables you are working with have the intended types.

It is very easy to trick TypeScript. Basically, any operation that can avoid the static analysis can potentially change the type of a variable without TypeScript noticing:

  • retrieving a property using bracket notation and a variable key
  • dynamic code evaluation via an HTML event attribute,setTimeoutor theFunction constructor for example
  • a global variable conflict with an external library or a browser extension
  • a built-in prototype that has been modified in an unintended way by a library or a polyfill

TypeScript developers try to avoid all these patterns and endorse best practices. However, this can lead to confusing issues because of the trust the developer puts in the static type system, forgetting that it is actually a dynamic script language that is run on the machine in the end.

There is another approach to type safety in JS that has been left behind and probably deserves more attention: strong type-checking in JavaScript itself.

Since ECMAScript 5 and property getters/setters, we can control the assignments made on an object property. Take this example :

let _name = "joe";
const user = {
get name(){ return _name },
set name(value){
if(typeof value !== "string")
throw new TypeError("Invalid type")
_name = value
}
}

user.name = 666; // throws a TypeError

Simple checks can be done on an object property, provided that you know all the property names and that they are always defined on the object. Setters have also other flaws : they don’t catch all operations done on object properties, and can be easily overruled with methods like Object.defineProperty().

This brings us to one of the most undervalued features of ES6/ES2015: the Proxy object. Proxies wrap around an object and act as a transparent pass-through. Developers can intercept all the operations done on this object by setting up traps. This is exactly what is needed to bring strong type-checking to our code.

Let’s rewrite our previous code using Proxy:

const user = new Proxy({ name: "joe" }, {
set (obj, key, value) {
if(key === "name" && typeof value !== "string")
throw new TypeError("Invalid type")

return Reflect.set(obj, key, value)
}
})

user.name = 666; // throws a TypeError

Here, we only defined the set trap, but we could have done the same for defineProperty , deleteProperty and any other traps which can mutate our property value. Note the Reflect API which retrieves the default behaviour of the intended operation.

The key difference with getters / setters is the Proxy not required to know the property name to catch the performed operations. This makes it possible to add type-checking on dynamic yet-to-be-defined properties, and also to write more generic utility functions:

function typecheck(obj, definition){
return new Proxy(obj, {
set (obj, key, value) {
if(key in definition && typeof value !== definition[key])
throw new TypeError("Invalid type")

return Reflect.set(obj, key, value)
}
})
}

class User {
constructor(){
return typecheck(this, {
name: "string",
age: "number"
})
}
}

let joe = new User();
joe.age = "twelve"; // throws a TypeError

Proxies can work on any type of object and may have a trap for almost any operation on a variable. This includes function calls with anapply trap. It is easy to imagine building an entire type-checking system on top of it. And this is exactly what I have been doing with ObjectModel over the last year.

// Basic Models
const PositiveInteger = BasicModel(Number)
.assert(Number.isInteger)
.assert(n => n >= 0, "should be greater or equal to zero")

// Object Models
class Person extends ObjectModel({
name: String,
age: PositiveInteger
}){
greet(){ return `Hello I'm ${this.name}` }
}

// Function Models
Person.prototype.greetSomeone = FunctionModel(Person).return(String)(function(person){
return `Hello ${person.name}, I'm ${this.name}`
})

// and models for Arrays, Maps, Sets...

Models are basically an improved version of the typecheck function in the previous code example. Their role is to ensure your variables are conform to the model definition, similarly to TypeScript Interfaces.

This is just the tip of the iceberg. Because all of this is done at runtime, we can imagine all kind of usecases which are not possible with static type-checking solutions :

  • validate a JSON shape coming from a REST API, and automatically cast the nested data into the appropriate JS classes
  • check the validity of content coming from localStorage or IndexedDB
  • perform feature detection by type-checking built-in browser API
  • add type definitions on the fly to external libraries coming from a CDN

Now that our types have been freed from static analysis, we can even imagine type definitions which change depending on the state of the application : adding new controls on the fly to a User instance once his permissions have changed, for example.

These are just a few of the many benefits of dynamic type-checking systems over the static checking. In addition to this, it doesn’t require learning a new language or adding a compilation step. It is just a small plain old JavaScript library.

Proxies now have decent browser support, I think it is time to broaden our horizons regarding type safety in JavaScript. TypeScript and Flow provide a great developer experience and ObjectModel does not intent to replace them, but there is room for innovation and experimenting with new approaches.

ObjectModel 3.0 has just been released. This is by far the most ambitious open-source project I have worked on, and I hope you will give it a try. I have already received great feedback

Thank you for this absolutely AWESOME package! Exactly what I was looking for. Since 99% of (my) debugging always boils done to sending/receiving messed up params, args , objects etc. you can imagine what a time saver this is going to be.

- but I am also open to criticism and debate. Please share your experience and opinions in the comments, and happy coding !

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.