Extend a class with a proxied constructor
I was working on migrating sequelize 4 to 5 when I found a strange behavior. We have a custom data type derived from STRING. It’s something like this.
class CUSTOM_STRING extends DataTypes.STRING {
toSql() {
return ...;
}
...
}
Although this worked fine with sequelize 4, I found it no longer worked with sequelize 5. The thing is, this overridden toSql was no longer called even when I created an instance of this class and call toSql on it.
const s = new CUSTOM_STRING();
s.toSql(); // This calls STRING.toSql instead of CUSTOM_STRING.toSql
After looking into sequelize, I found that all data type classes are wrapped in a proxy using classToInvokable.
I wrote a simple code that demonstrates this issue.
class X {
name() { return 'X'; }
}
const XP = new Proxy(X, {
apply(Target, thisArg, args) { return new Target(...args); },
construct(Target, args) { return new Target(...args); },
get(target, p) { return target[p]; }
});
class Y extends XP {
name() { return "Y"; }
}
const y = new Y();
console.log(y.name());
This prints X instead of Y. The reason is that this proxy has construct handler and it returns a new object of Target. This handler will be called when the constructor of Y calls super() implicitly and it’ll sets this to an object returned by the proxy handler.
class Y extends XP {
constructor() {
super();
console.log(this);
}
}
You’ll find this prints X {} instead of Y {}. Even though you newed Y, it returned an instance of X.
To workaround this, you can extend Y from XP.prototype.constructor instead of XP (Note that X itself is invisible in the sequelize case). This works because a prototype of a class is the class itself and the proxy’s get handler just returns a target’s property.
class Y extends XP.prototype.constructor {
name() { return "Y"; }
}
const y = new Y();
console.log(y.name());
This prints Y as you’d expect.