网站搜索

如何将 JSON 数据导入 TypeScript 的 Map 类型?


我正在尝试导入可能包含某个属性中存在或缺失的映射的 JSON 数据,并且认为表示此数据的正确数据类型是 Map<string, number>,但是当我尝试这样做时会出现错误。

我的 JSON 文件,data.json,长这样:

{
    "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
            }
        }
    ]
}

我的 TypeScript 代码 Data.ts,尝试读取它的代码如下:

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;
}

但最后一行,public datas:Data[]=data.datas;,发生了错误。

有一种正确的方式来导入像这样的数据吗?

最终的目标是实现三件事:

  1. 每当 entries 出现时,它应该接受 某种 验证,以确保它仅包含 number 类型的属性;这些属性是什么程序员不知道,但对最终用户来说是相关的。
  2. 如果 JSON 文件中缺少 config,构建 Data 对象时应提供一个默认值(这里是 "origin")
  3. 这个数据的分配应该尽量少使用样板代码。如果将来 Data 更新了一个新属性(而 Data.json 的数据可能会或可能不会相应更新),我不想必须修改 DataManager.data 接收这些值的方式

这是可能的吗?如果可能,该如何正确地编写代码来实现这一点?

答案#1

实现这一点最轻量的方法不是为你的数据创建或使用类。你可以改用普通的 JavaScript 对象,并且只需根据你的使用场景强类型描述它们的类型。因此,不是使用 Data 类,你可以用接口;而不是使用带有 string 类型键的 Map 类实例,你可以直接使用具有字符串索引签名的普通对象来表示你已有的数据类型:

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

要创建一个有效的 Data,你不需要在任何地方使用 new;只需创建一个带有正确类型的 name、config 和 entries 属性的对象字面量即可。entries 属性是 { [k: string]: number },这意味着你不需要知道或关心键是什么(除了它们是 string 而不是 symbol),但是这些键对应的属性值应该是 number。


有了这个定义,让我们以符合你三项标准的方式将 data.datas 转换为 Data[]:

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. 上面的代码将 entries 属性设置为传入数据中 entries 属性的过滤版本(如果存在)。(如果 entries 不存在,我们使用一个空对象 {})。过滤函数是 onlyNumberValues(),它通过 Object.entries() 方法将对象拆分为条目,使用用户定义的类型保护函数过滤这些条目,然后通过 Object.fromEntries() 方法将它们重新打包为对象。该函数的具体实现细节可以更改,但核心思想是你在这里执行所需的任何验证/转换。

  2. JSON 文件中可能缺失的任何必填属性都应赋予默认值。我们通过创建一个以这些默认属性开始的对象字面量来实现此目的,然后将 JSON 对象中的属性展开到该对象中。我们在上面的 config 属性中就是这样做的。如果 JSON 对象有 config 属性,它将在展开时覆盖默认值。(在最后,我们显式地添加 entries 属性,以用过滤后的版本覆盖对象中的值。)

  3. 因为我们已经展开了 JSON 对象,任何添加到 JSON 对象的属性都会自动被加入。只要记得为这些新属性指定任何默认值,如果它们是必需的。

让我们确保这按预期工作:

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
  }
}] */

看起来不错。

代码的 Playground 链接

相关文章