I'm trying to extend a class from an external library that contains methods which use its constructor in the input before starting any operation since it can have multiple types. I want to create a Class that works similarly to it but where the input is sanitized.
class ParentClass {
constructor(value: ParentClass.ValueType) {
// ...
}
methodUsingConstructor(otherValue: ParentClass.ValueType) {
const x = new this.constructor(otherValue)
// do some stuff here with x ...
return result;
}
// ...
}
If I do
class SafeParentClass extends ParentClass {
constructor(value: ParentClass.ValueType) {
// Do some input sanitizing ...
super(sanitizedValue);
}
}
I can use new SafeParentClass(value)
. However, I will get an object with the constructor of the ParentClass. That means that if I do (new SafeParentClass(value1)).methodUsingConstructor(value2)
, then value2
is not sanitized and I get an error.
How can I create a SafeParentClass
such that I can call the methods of the ParentClass
and they will have a constructor that sanitizes the input?
Example:
import { Decimal } from 'decimal.js';
class SafeDecimal extends Decimal {
constructor(value: Decimal.Value) {
if (typeof value === 'string') {
super(parseFloat(value)); // will do super('NaN') if value is not parseable
} else {
super(value);
}
}
}
And then testing it
describe('safeDecimal', () => {
it('should return NaN output with invalid input', () => {
expect(new SafeDecimal('123123').add('').toString()).toEqual('NaN');
});
});
That throws a DecimalError
.
The problem is the way decimal.js
is written. It overwrites the constructor
property on each instance to be, specifically, Decimal
(rather than allowing it to be inherited from the prototype, which would be the normal thing). It does that on what is currently line 4,290 in decimal.mjs
(x.constructor = Decimal;
). The comment on it says it does that to shadow the inherited one "...which is Object
." (The normal thing would be to fix the inherited one insetad.)
You can fix that by overriding what decimal.js
does:
class SafeDecimal extends Decimal {
constructor(value: Decimal.Value) {
if (typeof value === "string") {
super(parseFloat(value)); // will do super('NaN') if value is not parseable
} else {
super(value);
}
this.constructor = SafeDecimal; // <=== Here
}
}
Then, your constructor is used and your calculation example works (resulting in NaN
). I haven't tested other things.
In the normal case, you wouldn't need this. It's only necessary because of the specific way decimal.js
is written.
Having said that, you might want to fork it and do the changes you think you need to do in your fork instead.
Finally, you might want to consider doing something more robust than parseFloat
, since parseFloat
will happily return 123
for the string "123asdlfkjasldf"
(whereas it would probably be more reasonable to throw an error or return NaN
). Here's an answer going into your various options for the tools you might use to build something more robust.