0

Having the following json

[
    {
      "Value": "100000000",
      "Duration": 1
    },
    {
      "Value": "100000001",
      "Duration": 2
    },
    {
      "Value": "100000002",
      "Duration": 3
    },
    {
      "Value": "100000003",
      "Duration": 5
    },
    {
      "Value": "100000004",
      "Duration": 0
    },
    {
      "Value": "100000005",
      "Duration": 8
    },
    {
      "Value": "100000006",
      "Duration": 10
    }
  ]

and the following definition

interface Duration {
  value: string
  duration: number
}

I would like to have a method on Duration interface to be available in all objects

durationInSeconds():number {
  return duration*1000
}

The usage scenario would be:

const all = previousJsonContent
const durations:Duration[] = parseAsObjects(all) // JSON.parse(all)
const firstInSeconds = durations[0].durationInSeconds()

Typescript approach

What is idiomatic to typescript? What is the least intrusive way? Should I use assign, prototype, mixins? The problem is that deserialized json is just a data container while I want to treat it as full Objects with methods assigned according to the types and ideally also deep, at fields that also should have a type.

Scala approach

In scala I would create a value class wrapper around the data without performance penalty and use some implicit magic like in https://www.baeldung.com/scala/rich-wrappers

raisercostin
  • 8,777
  • 5
  • 67
  • 76

2 Answers2

1

You need to turn a bunch of POJO's (Plain Old JavaScript Objects) into a class with methods specialized for this kind of object. The idiomatic way is to create a class that takes the POJO's data in some way (since I'm lazy I just pass the entire thing). TypeScript doesn't change how you approach this - you just need to add type annotations.

const data = [{"Value":"100000000","Duration":1},{"Value":"100000001","Duration":2},{"Value":"100000002","Duration":3},{"Value":"100000003","Duration":5},{"Value":"100000004","Duration":0},{"Value":"100000005","Duration":8},{"Value":"100000006","Duration":10}];

class Duration {
    /* private data: { Value: string; Duration: number } */

    constructor(data/* : { Value: string; Duration: number */) {
        this.data = data;
    }
 
    durationInSeconds() {
        return this.data.Duration * 1000;
    }
}

const parsed = data.map((datum) => new Duration(datum));

console.log(parsed[0].durationInSeconds());

For convenience you may add a method like this:

    static new(data) {
        return new Duration(data);
    }

Then it'll look cleaner in the map:

const parsed = data.map(Duration.new);
kelsny
  • 23,009
  • 3
  • 19
  • 48
  • Couldn't the constructor be simplified to `constructor(private data) {}`? But I can see how that might not be very readable.. – Jacob Nov 06 '22 at 13:36
  • 1
    @Jacob Sure, but I'm using stack snippets to keep everything in the site - JS doesn't have that syntax yet :p – kelsny Nov 06 '22 at 13:37
  • It would be great if they added TS to the snippets thing. I mean, they already have babel in there, why not add babel-preset-typescript? – Jacob Nov 06 '22 at 13:39
  • Likely because TS has lots of configuration and it would be pretty complicated to get working. Also, TS isn't as popular as JS or other languages. – kelsny Nov 06 '22 at 13:40
  • Okay that makes sense. But it would be inconsistent with what is there--they still have AngularJS available in the snippets menu – Jacob Nov 06 '22 at 13:42
  • AngularJS isn't https://angular.io, they're actually pretty different. The other frameworks listed are the most common and aren't TypeScript-oriented. – kelsny Nov 06 '22 at 13:46
  • No, I was talking about AngularJS, which is included in the menu. The other frameworks are React 16 (outdated) and Vue 2.x (outdated) so they really should update it. But probably better to talk about it on meta – Jacob Nov 06 '22 at 13:48
1

The problem is that deserialized json is just a data container...

That's right. In order to deserialize JSON to a class instance with methods, you need to tell deserializer which class is used to create the instance.

However, it's not a good idea to define such class information using interface in TypeScript. In TypeScript, interface information will be discarded after compilation. To define class during deserialization, you need class:

class Duration {
  value: string;
  duration: number;
  durationInSeconds(): number {
    return this.duration*1000;
  };
}

To deserialize JSON into object with methods (and "ideally also deep"), I've made an npm module named esserializer to solve this problem: save JavaScript/TypeScript class instance values during serialization, in plain JSON format, together with its class name information:

const ESSerializer = require('esserializer');
const serializedText = ESSerializer.serialize(yourArrayOfDuration);

Later on, during the deserialization stage (possibly on another machine), esserializer can recursively deserialize object instance, with all Class/Property/Method information retained, using the same class definition:

const deserializedObj = ESSerializer.deserialize(serializedText, [Duration]);
// deserializedObj is a perfect copy of yourArrayOfDuration
shaochuancs
  • 15,342
  • 3
  • 54
  • 62