Home > Blockchain >  How to Import JSON data to typescript Map<K, V> type?
How to Import JSON data to typescript Map<K, V> type?

Time:03-31

I am trying to import json data that might include present or absent mappings within one of the properties, and figured the correct data type to represent this was Map<string, number>, but I'm getting an error when I try to do this.

My JSON file, data.json, looks like this:

{
    "datas": [
        {
            "name":"test1",
            "config":"origin",
            "entries": {
                "red":1,
                "green":2
            }
        }
        ,
        {
            "name":"test2",
            "config":"remote",
            "entries": {
                "red":1,
                "blue":3
            }
        }
        ,
        {
            "name":"test3",
            "entries": {
                "red":1,
                "blue":3,
                "purple":3
            }
        }
    ]
}

My typescript code, Data.ts, which attempts to read it looks like this:

import data from './data.json';

export class Data {
    public name:string;
    public config:string;
    public entries:Map<string, number>;
    constructor(
        name:string,
        entries:Map<string, number>,
        config?:string
    ) {
        this.name = name;
        this.entries = entries;
        this.config = config ?? "origin";
    }
}

export class DataManager {
    public datas:Data[] = data.datas;
}

But that last line, public datas:Data[] = data.datas;, is throwing an error.

Is there a proper way to import data like this?

The goal, ultimately, is to achieve three things:

  1. Any time entries is present, it should receive some validation that it only contains properties of type number; what those properties are is unknown to the programmer, but will be relevant to the end-user.
  2. If config is absent in the JSON file, the construction of Data objects should supply a default value (here, it's "origin")
  3. This assignment of the data should occur with as little boilerplate code as possible. If, down the line Data is updated to have a new property (and Data.json might or might not receive updates to its data to correspond), I don't want to have to change how DataManager.data receives the values

Is this possible, and if so, what's the correct way to write code that will do this?

CodePudding user response:

The lightest weight approach to this would not be to create or use classes for your data. You can instead use plain JavaScript objects, and just describe their types strongly enough for your use cases. So instead of a Data class, you can have an interface, and instead of using instances of the Map class with string-valued keys, you can just use a plain object with a string index signature to represent the type of data you already have:

interface Data {
    name: string;
    config: string;
    entries: { [k: string]: number }
}

To make a valid Data, you don't need to use new anywhere; just make an object literal with name, config, and entries properties of the right types. The entries property is { [k: string]: number }, which means that you don't know or care what the keys are (other than the fact that they are strings as opposed to symbols), but the property values at those keys should be numbers.


Armed with that definition, let's convert data.datas to Data[] in a way that meets your three criteria:

const datas: Data[] = data.datas.map(d => ({
    config: "origin", // any default values you want
    ...d, // the object    
    entries: onlyNumberValues(d.entries ?? {}) // filter out non-numeric entries
}));

function onlyNumberValues(x: { [k: string]: unknown }): { [k: string]: number } {
    return Object.fromEntries(
        Object.entries(x).filter(
            (kv): kv is [string, number] => typeof kv[1] === "number"
        )
    );
}
  1. The above sets the entries property to be a filtered version of the entries property in the incoming data, if it exists. (If entries does not exist, we use an empty object {}). The filter function is onlyNumberValues(), which breaks the object into its entries via the Object.entries() method, filters these entries with a user-defined type guard function, and packages them back into an object via the Object.fromEntries() method. The details of this function's implementation can be changed, but the idea is that you perform whatever validation/transformation you need here.

  2. Any required property that may be absent in the JSON file should be given a default value. We do this by creating an object literal that starts with these default properties, after which we spread in the properties from the JSON object. We do this with the config property above. If the JSON object has a config property, it will overwrite the default when spread in. (At the very end we add in the entries property explicitly, to overwrite the value in the object with the filtered version).

  3. Because we've spread the JSON object in, any properties added to the JSON object will automatically be added. Just remember to specify any defaults for these new properties, if they are required.

Let's make sure this works as desired:

console.log(datas)

/* [{
  "config": "origin",
  "name": "test1",
  "entries": {
    "red": 1,
    "green": 2
  }
}, {
  "config": "remote",
  "name": "test2",
  "entries": {
    "red": 1,
    "blue": 3
  }
}, {
  "config": "origin",
  "name": "test3",
  "entries": {
    "red": 1,
    "blue": 3,
    "purple": 3
  }
}] */

Looks good.

Playground link to code

  • Related