Skip to the content.

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.