Currently I'm working on a typescript project and I'm really enjoying the type inference that TypeScript brings to the table. However - when getting objects from HTTP calls - I can cast them to the desired type, get code completion and call functions on them compile time, but those result in errors runtime
Example:
class Person{ name: string; public giveName() { return this.name; } constructor(json: any) { this.name = json.name; } } var somejson = { 'name' : 'John' }; // Typically from AJAX call var john = <Person>(somejson); // The cast console.log(john.name); // 'John' console.log(john.giveName()); // 'undefined is not a function'
Although this compiles nicely - and intellisense suggests me to use the function, it gives a runtime exception. A solution for this could be:
var somejson = { 'name' : 'Ann' }; var ann = new Person(somejson); console.log(ann.name); // 'Ann' console.log(ann.giveName()); // 'Ann'
But that will require me to create constructors for all my types. In paticular, when dealing with tree-like types and/or with collections coming in from the AJAX-call, one would have to loop through all the items and new-up an instance for each.
So my question: is there a more elegant way to do this? That is, cast to a type and have the prototypical functions available for it immediately?
Type casting in TypeScript can be done with the 'as' keyword or the '<>' operator.
io-ts is a runtime type checking library that provides utility methods to perform data validation and encodes/decodes the data being parsed. io-ts is essentially used to streamline data operations in TypeScript for runtime type checking.
JavaScript doesn't have a concept of type casting because variables have dynamic types. However, every variable in TypeScript has a type. Type castings allow you to convert a variable from one type to another. In TypeScript, you can use the as keyword or <> operator for type castings.
TypeScript types are not available at runtime.
The prototype of the class can be dynamically affected to the object:
function cast<T>(obj: any, cl: { new(...args): T }): T { obj.__proto__ = cl.prototype; return obj; } var john = cast(/* somejson */, Person);
See the documentation of __proto__
here.
Take a look at the compiled JavaScript and you will see the type assertion (casting) disappears because it's only for compiling. Right now you're telling the compiler that the somejson
object is of type Person
. The compiler believes you, but in this case that's not true.
So this problem is a runtime JavaScript problem.
The main goal to get this to work is to somehow tell JavaScript what the relationship between the classes are. So...
There's many ways to solve it, but I'll offer one example off the top of my head. This should help describe what needs to be done.
Say we have this class:
class Person { name: string; child: Person; public giveName() { return this.name; } }
And this json data:
{ name: 'John', child: { name: 'Sarah', child: { name: 'Jacob' } } }
To map this automatically to be instances of Person
, we need to tell the JavaScript how the types are related. We can't use the TypeScript type information because we will loose that once it's compiled. One way to do this, is by having a static property on the type that describes this. For example:
class Person { static relationships = { child: Person }; name: string; child: Person; public giveName() { return this.name; } }
Then here's an example of a reusable function that handles creating the objects for us based on this relationship data:
function createInstanceFromJson<T>(objType: { new(): T; }, json: any) { const newObj = new objType(); const relationships = objType["relationships"] || {}; for (const prop in json) { if (json.hasOwnProperty(prop)) { if (newObj[prop] == null) { if (relationships[prop] == null) { newObj[prop] = json[prop]; } else { newObj[prop] = createInstanceFromJson(relationships[prop], json[prop]); } } else { console.warn(`Property ${prop} not set because it already existed on the object.`); } } } return newObj; }
Now the following code will work:
const someJson = { name: 'John', child: { name: 'Sarah', child: { name: 'Jacob' } } }; const person = createInstanceFromJson(Person, someJson); console.log(person.giveName()); // John console.log(person.child.giveName()); // Sarah console.log(person.child.child.giveName()); // Jacob
Playground
Ideally, the best way would be to use something that actually reads the TypeScript code and creates an object that holds the relationship between classes. That way we don't need to manually maintain the relationships and worry about code changes. For example, right now refactoring code is a bit of a risk with this setup. I'm not sure that something like that exists at the moment, but it's definitely possible.
Alternative Solution
I just realized I already answered a similar question with a slightly different solution (that doesn't involve nested data though). You can read it here for some more ideas:
JSON to TypeScript class instance?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With