I'm working on a utility library for use with a complex API model. We receive JSON-parsed objects from the wire with some kind of guarantee about their structure, as follows:
// Known structure of the message:
interface InputBox {
top: number;
height: number;
left: number;
width: number;
}
interface InputObj {
box: InputBox
}
// User code (outside our scope) does something like:
const inputObj: InputObj = JSON.parse(
'{ "box": { "top": 0, "height": 10, "left": 1, "width": 2, "color": "red" } }'
);
My goal is to create some view of the object which:
- Adds some utility functions & properties
- Stringifies to the same/compatible JSON as the input
- Behaves consistently when treated as mutable
- Preserves any unexpected attributes, if possible
For example, user code might look like:
// In-place or new obj constructor would both be fine:
const easyObj = myCoolLibrary(inputObj);
easyObj.box.top = 5;
console.log(easyObj.box.getBottom()); // '15'
JSON.stringify(easyObj);
// -> { "box": { "top": 5, "height": 10, "left": 1, "width": 2, "color": "red" } }
// (Note box.color still present, although missing from the input interface)
After reading a little around options, it seems like:
- There could be a nice in-place solution using Object.setPrototypeOf to directly overwrite prototypes in the input, but it's quite recent and not sure if polyfilling for older browsers would work.
- Creating new objects (with classes/prototypes) and cloning properties across with Object.assign could work since the input objects are known to be simple/boring JSON-able
Objects... Still needs polyfill on IE, but maybe is a bit more standard?
The thing is, even if these methods are viable, I can't see how they might be implemented in a way that keeps TypeScript happy?
For e.g:
class MyCoolBox implements InputBox {
constructor(box: InputBox) {
Object.assign(this, box);
}
getBottom() {
return this.top + this.height;
}
}
// > Class 'MyCoolBox' incorrectly implements interface 'InputBox'.
// (and doesn't recognise presence of .top, .height)
Object.setPrototypeOf(inputObj.box, MyCoolBox);
inputObj.box.getBottom();
// > Property 'getBottom' does not exist on type 'InputBox'
// Doesn't recognise the change of interface.
Am I missing some sensible way to do this that TypeScript could understand? It seems a reasonable ask to just decorate a JSON-parsed object (of known interface) with some methods!