0

In daily life I need to read some json from ajax and cast it to some Typed object (including its METHODS). On internet I found and use following code to type casting:

export class Obj 
{
    public static cast<T>(obj, type: { new(...args): T} ): T 
    {
        obj.__proto__ = type.prototype;
        return obj;
    }
}

As example It can be use in following way:

let objFromJson = { id: 666, name: "love" };
let building: Building = null; 
building = Obj.cast(objFromJson, Building);

// On this point constructor for Building is not call - this is
// correct because we not create object but only make type casting

building.test('xx');

// on this point on console we should get:
// > "building.test a:xx, name:love"

// so object 'building' indeed have methods of Building Class

where (example is from 'head')

export class Building {

    constructor(
        public id: number,
        public name: string,
    ) {
        console.log('building.constructor: id:' + id + ', name' + name);
    }

    public test(a) {
        console.log('building.test a:' + a + ', name:' + this.name);
    }

}

Additional info: Instead of using cast<T>(obj, type: { new(...args): T} ): T we can use just cast<T>(obj, type): T but I read that second version will cause problem with arrow functions (https://stackoverflow.com/a/32186367/860099) - I don't understand why - ?

Questions: I not really understand how method Obj.cast works (and for instance how I can use ...args on calling it) - can someone explain it? Do someone know alternative function but not for casting but for CREATE object (so call constructor) form json data in similar handy way (eg. building = Obj.create(objFromJson, Building);

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345

1 Answers1

2

How cast works

Javascript uses prototypical inheritance. The __proto__ property, represents the prototype of the current object. This is what determines the type of the given object

For objects created using an object literal, this value is Object.prototype. For objects created using array literals, this value is Array.prototype. For functions, this value is Function.prototype. For objects created using new fun, where fun is one of the built-in constructor functions provided by JavaScript (Array, Boolean, Date, Number, Object, String, and so on — including new constructors added as JavaScript evolves), this value is always fun.prototype. For objects created using new fun, where fun is a function defined in a script, this value is the value of fun.prototype.

So when you change the __proto__ you change the prototype chain for the object basically changing it's type.

type: { new(...args): T} it the way you can represent a constructor function in typescript. As the above quote says for an object constructed by a function (such as type) the __ptoto__ should be the prototype of the function.

So when setting the __proto__, cast basically simulates the fact that the object was constructed using the type constructor function passed as a parameter.

Problems with this approach

The problems is that the constructor doesn't actually get invoked, you are just simulating the fact that the object was created using the given constructor. So any initialization that occurs in the constructor does not get executed. Arrow functions for example need to capture this, so they are not set on the prototype of the constructor function but rather on the instance of the object during the invocation of the constructor so if the constructor does not invoked, arrow functions are not initialized:

export class Building {
    private otherField : string;
    constructor(
        public id: number,
        public name: string,
    ) {
        console.log('building.constructor: id:' + id + ', name' + name);
        this.otherField = name+id;
        // Ts adds in the code for initializing arrow functions in JS, but the idea is the same, this is where it would happen
    }

    public arrow = ()=> {};
    public test(a) {
        console.log('building.test a:' + a + ', name:' + this.name);

        // both fields below will be undefined if cast was used.
        console.log('building.otherField' + this.otherField + ', arrow:' + this.arrow);         }
}

Alternative to cast

An alternative would be to create a new instance of the class and use Object.assign to assign the properties from the json object. At first glance this may seem slower, but the documentation says changing the __ptoto__ is very slow and not recommended

Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine.

export class Building {
    public id: number;
    public name: string;
    constructor(data: Partial<Building>){
        Object.assign(this, data)
        console.log('building.constructor: id:' + this.id + ', name' + this.name);
    }

    public test(a) {
        console.log('building.test a:' + a + ', name:' + this.name);
    }

}
let objFromJson = { id: 666, name: "love" };
let building: Building = new Building(objFromJson);

If the class does not have any methods, and maybe you can change your design so it does not, then I would just use an interface to type the JSON object and keep using the original JSON object.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • @Kamil Kiełczewski Added more info to take into account the questions you added after posting. If the answer is unclear somewhere, let me know. – Titian Cernicova-Dragomir Feb 05 '18 at 06:31
  • Tank you for your answer. `Alternative to cast` - your proposition unfortunately is not-handy - because if I use it I need to add `Object.assign(this, data)` to constructor (and change its parameters) on each `model-class` (like Building, Floor etc.). Using `Obj.cast` I don't need to touch any "model-class". – Kamil Kiełczewski Feb 05 '18 at 07:04
  • @KamilKiełczewski you can use `Object.assign` outside the constructor, and pass undefined for the parameters (supposing you don't use them in the constructor). Still you should consider the constructor version. It is pretty common practice to have such a constructor, and `new Building({ id: 10, name: "foo"})` is more readable then `new Building(10, "foo")` especially if the number of parameters grows – Titian Cernicova-Dragomir Feb 05 '18 at 07:11