Inspired by this post of Mark Galea about mapping JSON objects to classes I asked myself the question: Would it be possible to use this technique to map JSON objects to Knockout view models? This is even more so useful since the knockout.mapping plugin is not being maintained. Also, any knockout mapping library causes redundancy in your code: You have to specify types and properties twice in a worst-case scenario: Once in the class, once in the mapping code. There is surely a better way.
TL;DR of the original post
The original post describes how it is possible to create a json to object mapping by using the decorators feature of Typescript. Decorators are described in an proposal for the upcoming ES7, and already implemented in Typescript. Because the spec and the related metadata API spec is not final, these features are hidden behind a flag of the compiler.
When including the experimental Reflect API in your code, Typescript emits the decorators via the Reflect API. Also, Typescript emits the class constructor via the
design:type field. This allows to easily look up the appropiate constructor for an property. This doesn’t work for array properties unfortunately, and manually specifying the class constructor is needed for that.
For this post I have set up a very simple case for implementing a json-to-knockout mapping:
We have a survey system and our survey editor is written in typescript. Our data is represented by
We want to map the JSON generated from the server seamlessly to our classes.
Let’s recap how knockout observables work. Knockout observables are functions which either set a value or return a value based on how many arguments were provided to the observable. Knockout observable arrays are just like normal observables, but are enhanced with methods which provide array functions. And - of course - should contain an array as value.
Knockout in Typescript
Knockout is very easily used in Typescript in a strongly-typed manner:
The type definition of an observable is simply a generic interface:
Normally, the class constructor is emitted via the
design:type metadata field. How does this work for observables? Let’s take a look at the generated code:
Unfortunately, the generic type parameter is not encoded in the metadata. This feature was actually requested but not built because it was too complex.
Implementing the mapping code
Let’s abstract setting a property into seperate interface:
We implement this for both regular and observable properties:
And now during mapping, we only have to check whether our target property is an observable:
We have one thing left to check when mapping: The original code checks if the target property is an array. This won’t work if we map to an observable array. Let’s modify the code a bit:
We check if the value contained by the observable is an array. If so, we still execute the mapping code.
Decorating our view models
As we’ve discovered, the generic type parameters are not encoded in the metadata of a property. This means we’re going to do it ourselves by explicitly specifying the class name when decorating an observable that is not an primitive:
And… we’re done! Check out the entire code solution and working demo on GitHub.
This solution is not without its limitations. First of all is that one needs to specify the correct class for each complex observable property. This is a limitation of the Typescript compiler, though it may be possible to work around it by talking to the Typescript compiler directly.
Third limitation, just like the original solution, is that it isn’t very suitable for mapping multiple json objects to the same target object. If you want that, you’re best of with custom mapping code.
We’ve seen how we can adapt the mapping technique to allow mapping to Knockout view models. While it is not without its limitations, its still a very interesting approach.