Currently, most of the solutions to poor type safety in JavaScript are based on static type checking and type inference. TypeScript and Flow extend JavaScript by adding statically typed annotations, validate your code at compile time, and use the same abstract syntax tree to output the final JS code. Because ides can rely on static analysis to provide auto-completion and development assistance, this can be very effective in improving the developer experience. However, there is still one major drawback with regard to type safety: once compiled in JavaScript and run in a browser, there is no longer a guarantee that the variables used have the expected type.
It’s easy to fool TypeScript. Basically, anything that avoids static analysis can potentially change the type of a variable without notifying TypeScript:
- The property is retrieved using square brackets and a variable representing the property
- through
HTML
Event attributes,setTimeout
orFunction
Constructor for dynamic code evaluation - Global variable conflicts with external library or browser extension
- A built-in prototype that has been accidentally modified by library or Polyfill,
TypeScript developers try to avoid these patterns as best practices. However, because of developers’ faith in the static type system, this can lead to some confusing problems, forgetting that it is actually a dynamic scripting language running on a computer at the end of the day.
There is another type safety approach in JS that has been forgotten and probably deserves more attention: strong type checking in JavaScript itself.
Because of ECMAScript5 and the property getters/setters, we can control the allocation of object properties. Look at this example:
let _name = 'joe';
const user = {
get name() {
return _name
},
set name(value) {
if (typeofvalue ! = ='string') {
throw new Error('Incoming type is not a string')}else {
_name = value
}
}
}
user.name = 'hello'
user.name = 123 //Error: The incoming type is not a string
Copy the code
You can perform simple type checking on object properties as long as you know all the property names of the object and always define them on the object. Setters also have another drawback: they can’t capture all operations on Object properties and can be easily overturned with methods like Object.defineProperty().
This brings us to one of the most underrated features of ES6/ES2015: Proxy objects. Proxy wraps the target object and acts as a transparent delivery. Developers can set traps to intercept all operations on the object. This is exactly what we need to bring powerful type checking to our code.
Let’s rewrite the previous code with Proxy:
const user = new Proxy({_name: 'joe'}, {
set(target, p, value, receiver) {
if (p === '_name' && typeofvalue ! = ='string') {
throw new Error('Incoming type is not a string')}return Reflect.set(target, p, value)
}
})
user._name = 'hello'
user._name = 456 //Error: The incoming type is not a string
Copy the code
Here, we only intercept the SET operation, but we can also intercept defineProperty, deleteProperty, and any other traps that can change the value of our property.
The main difference from getters/ setters is that Proxy does not need to know the property name to capture the action being performed. This allows you to type check dynamic properties that have not yet been defined, and you can write more general-purpose utility functions:
function checktype(obj, definition) {
return new Proxy(obj, {
set(obj, key, value, receiver) {
if (key in definition && typeofvalue ! == definition[key]) {throw new Error(`${key}The type should be:${definition[key]}`)}return Reflect.set(obj, key, value)
}
})
}
class User {
constructor(name, age) {
/ / this is the instance
return checktype(this, {
name: 'string'.age: 'number'}}})let joe = new User()
joe.name = 'joe';
joe.age = '12' //Error: age should be number
Copy the code
Note: you can only use let Joe = new User(); Joe. name = ‘Joe’ instead of let Joe = new User({name: 11,age: ’23’}), because it does not trigger Proxy set interception, does not write attributes to this object, and returns an empty object with no attributes User {}
Proxy can handle any type of object, and probably intercept almost any operation on a variable. This includes function being called with apply, and you can imagine building a complete type checking system based on these. And that’s exactly what ObjectModel did 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 The ${this.name}`}}// Function Models
Person.prototype.greetSomeone = FunctionModel(Person).return(String) (function(person){
return `Hello ${person.name}, I'm The ${this.name}`
})
// and models for Arrays, Maps, Sets...
Copy the code
The model is basically an improved version of the type checking functionality from the previous code example. Similar to TypeScript interfaces, they ensure that variables conform to model definitions.
This is just the tip of the iceberg. Because all of this is done at runtime, we can imagine all the use cases that a static type-checking solution cannot implement:
- Validate the JSON form from the REST API and automatically convert the nested data to the appropriate JS classes
- Check the validity of content from localStorage or IndexedDB
- Function checking is performed through the type checking built-in browser API
- Quickly add type definitions to external libraries from the CDN
Now that our types have been freed from static analysis, we can even imagine type definitions changing based on the state of the application: for example, new controls are added to the User instance as soon as User permissions change.
If you can fq, watch the video dz.date /au3F
These are just a few of the many advantages that dynamic type checking systems have over static checking. Beyond that, it doesn’t require learning a new language or adding compilation steps. It’s just a small, generic JavaScript library.
Proxy now has decent browser support, and I think it’s time to expand our understanding of JavaScript type safety. TypeScript and Flow provide a great developer experience, and ObjectModel is not intended to replace them, but there is still room to innovate and try new approaches.
ObjectModel is a dynamic type-checking JS library, which relies on ES6 Proxy. This shows that Proxy is really powerful. You can look at the source code of this library to learn how to use Proxy in practical work. Now ObjectModel 4.0 has been released, using ES Module, using ES2018 written, so the source code is also a good example of learning JS.
Http://medium.com/sylvainpv/…