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:
- Any time
entries
is present, it should receive some validation that it only contains properties of typenumber
; what those properties are is unknown to the programmer, but will be relevant to the end-user. - If
config
is absent in the JSON file, the construction ofData
objects should supply a default value (here, it's "origin") - 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 howDataManager.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 string
s as opposed to symbol
s), but the property values at those keys should be number
s.
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"
)
);
}
The above sets the
entries
property to be a filtered version of theentries
property in the incoming data, if it exists. (Ifentries
does not exist, we use an empty object{}
). The filter function isonlyNumberValues()
, which breaks the object into its entries via theObject.entries()
method, filters these entries with a user-defined type guard function, and packages them back into an object via theObject.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.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 aconfig
property, it will overwrite the default when spread in. (At the very end we add in theentries
property explicitly, to overwrite the value in the object with the filtered version).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.