javascript - Construyendo una clase envolvente re-serializable en TypeScript
Estoy trabajando en una biblioteca de utilidades para usar con un modelo API complejo. Recibimos objetos analizados desde JSON con algún tipo de garantía sobre su estructura, de la siguiente manera:
// 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" } }'
);
Mi objetivo es crear alguna vista del objeto que:
- Agrega algunas funciones y propiedades de utilidad
- Se convierte a JSON igual/compatible con el de entrada
- Se comporta de manera consistente cuando se trata como mutable
- Conserva cualquier atributo inesperado, si es posible
Por ejemplo, el código del usuario podría verse así:
// 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)
Después de leer un poco sobre las opciones, parece que:
- Podría haber una buena solución en el lugar usando Object.setPrototypeOf para sobrescribir directamente los prototipos en la entrada, pero es bastante reciente y no estoy seguro de si un polyfill para navegadores más antiguos funcionaría.
- Crear nuevos objetos (con clases/prototipos) y clonar propiedades con Object.assign podría funcionar ya que se sabe que los objetos de entrada son simples/aburridos
Objectconvertibles a JSON... Aún necesita un polyfill en IE, pero tal vez sea un poco más estándar?
El caso es que, incluso si estos métodos son viables, no puedo ver cómo podrían implementarse de una manera que mantenga a TypeScript contento.
Por ejemplo:
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.
¿Me estoy perdiendo de alguna manera sensata de hacer esto que TypeScript pueda entender? ¡Parece razonable pedir simplemente decorar un objeto analizado desde JSON (de una interfaz conocida) con algunos métodos!
Respuesta #1
En primer lugar, parece que tienes un malentendido sobre cómo funciona implements. Las propiedades son reconocidas, es solo que le dijiste al compilador que tu clase implementa esas propiedades, pero nunca las implementaste realmente. Sí, haces el Object.assign en el constructor, lo que logra el resultado deseado en tiempo de ejecución, pero el compilador no lo sabe.
Dado que obviamente no quieres detallar cada propiedad posible del InputBox en la clase, la solución es usar en su lugar el hecho de que las clases tienen interfaces con el mismo nombre vinculadas a ellas. Así, puedes hacer que la interfaz MyCoolBox sea un subtipo de la interfaz que deseas extender (es decir, el InputBox):
interface MyCoolBox extends InputBox { }
class MyCoolBox {
constructor(box: InputBox) {
Object.assign(this, box);
}
getBottom() {
return this.top + this.height;
}
}
En segundo lugar, esperas que setPrototypeOf se comporte como una especie de guardia de tipo, mientras que simplemente se define como:
ObjectConstructor.setPrototypeOf(o: any, proto: object | null): any
lo que significa que al cambiar el prototipo no cambias nada de lo que el compilador sabe sobre la forma del objeto box: para él, todavía es un InputBox. También existe un problema de que las class en JavaScript son principalmente un azúcar sintáctico para funciones + herencia basada en prototipos.
La configuración del prototipo de box a MyCoolBox fallará en tiempo de ejecución cuando intentes llamar al método getBottom porque no es estático, y solo estás configurando el lado estático de la clase de esta manera. Lo que realmente quieres es establecer el protótipo de MyCoolBox: esto configurará las propiedades de la instancia:
const myCoolLibrary = <T extends InputObj>(input: T) => {
Object.setPrototypeOf(input.box, MyCoolBox.prototype); //note the prototype
return input as T & { box: MyCoolBox };
};
const enchanced = myCoolLibrary(inputObj);
const bottom = enchanced.box.getBottom(); //OK
console.log(bottom); //10
Finalmente, según el ejemplo anterior, necesitas decirle al compilador que el tipo de salida es del tipo mejorado. Puedes hacer esto con una simple aserción de tipo (as T & { box: MyCoolBox }) como arriba o dejando que el compilador infiera el tipo mejorado por ti:
{
const myCoolLibrary = (input: InputObj) => {
return {
...input,
box: new MyCoolBox( input.box )
}
};
const enchanced = myCoolLibrary(inputObj);
const bottom = enchanced.box.getBottom(); //OK
console.log(bottom); //10
}
Patio de recreo
