首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >转换JSON的TypeScript函数

转换JSON的TypeScript函数
EN

Stack Overflow用户
提问于 2021-03-17 12:49:48
回答 1查看 62关注 0票数 0

我编写了一个函数,它以一个结构(类型为Struct)作为输入,该结构可以包含原始类型以及MapSet对象,并将其转换为可以被JSON序列化的结构。投入实例:

代码语言:javascript
复制
let a = 'hello'; // -> "hello"
let b = new Map<string, Set<number>>([['a', new Set<number>([1, 3])]]); // -> {"a": [1, 3]}
let c = {a: new Set<number[]>([[1, 2]])}; // -> {"a": [[1, 2]]}
let d = [{e: false}]; // -> [{"e": false}]

但是,我发现我的代码特别冗长,我真的不确定它的安全性:

代码语言:javascript
复制
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
type Struct = Json | Struct[] | { [key: string]: Struct } | Map<string, Struct> | Set<Struct>;

function isJson(test: any): test is Json {
    if (test == null || ['string', 'number', 'boolean'].indexOf(typeof test) != -1)
        return true;
    if (Array.isArray(test)) {
        // if at least one of the values is not JSON serializable, the array is not JSON-serializable
        for (let value of test)
            if (!isJson(value))
                return false;
        return true;
    }
    if (typeof test == 'object') {
        // if it is not a plain object, the object is not JSON-serializable
        if (Object.getPrototypeOf(test) != null && test.constructor != Object)
            return false;
        // if there are symbol properties, the object is not JSON-serializable
        if (Object.getOwnPropertySymbols(test).length > 0)
            return false;
        // if at least one of the values is not JSON serializable, the object is not JSON-serializable
        for (let [key, value] of Object.entries(test))
            if (!isJson(test[key]))
                return false;
        return true;
    }
    return false;
}

function toJson(struct: Struct) {
    let json: Json = null;
    if (isJson(struct))
        json = struct;
    else if (Array.isArray(struct) || struct instanceof Set) {
        json = [];
        let structCast = struct instanceof Set ? struct as Set<Struct> : struct as Struct[];
        for (let value of structCast)
            json.push(toJson(value));
    }
    else if (Object.getPrototypeOf(struct) == null || struct.constructor == Object || struct instanceof Map) {
        json = {};
        let structCast = struct instanceof Map ? struct as Map<string, Struct> : Object.entries(struct);
        for (let [key, value] of structCast)
            json[key] = toJson(value);
    }
    return json;
}

我对isJson函数特别恼火。没有办法摆脱它吗?我知道在TypeScript编译之后,大多数的排版功能都会丢失,但是有没有更好的方法来做我想做的事情呢?

谢谢你的帮助。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2021-03-17 14:08:09

我将假设您正在编写TypeScript代码,而toJson()的用户也在编写TypeScript代码。如果这不是真的,而且如果toJson()函数是由某个纯JavaScript代码调用的,那么您将失去任何编译时间保证,并且需要根据您的需要向toJson()添加尽可能多的运行时检查。从现在开始,我假设所有相关代码都将通过编译步骤。

在这种情况下,您可能不再担心在isJson()中处理的一些奇怪的边缘情况。如果TypeScript Struct类型定义已经禁止某些内容,那么您不需要编写运行时代码来处理它。另一方面,Struct允许但不允许的任何内容都需要运行时检查。例如,在TypeScript中通常不可能说“拒绝具有任何symbol-valued键的对象类型”。您可以使用仿制药使编译器拒绝任何已知具有此类键的对象类型,但这要复杂得多,而且通常情况下,像{a: string}这样的类型的对象可能也可能没有其他未知键。我建议,如果您确实需要这样的检查,可以使用throw语句进行检查,因为您无论如何都无法在运行时之前捕获它们(除非您希望toJson()返回Json | undefined或其他一些失败标记)。

因此,让我们通过将我们需要的isJson()检查合并到toJson()函数并消除isJson()来解决这个问题。这里有一个可能的toJson()实现

代码语言:javascript
复制
function toJson(struct: Struct): Json {
  if (struct === null || typeof struct === "string" ||
    typeof struct === "number" || typeof struct === "boolean") {
    return struct;
  }
  if (Array.isArray(struct)) {
    return struct.map(toJson);
  }
  if (struct instanceof Set) {
    return Array.from(struct).map(toJson);
  }
  if (struct instanceof Map) {
    return Object.fromEntries(
      Array.from(struct).map(([k, v]) => [k, toJson(v)])
    );
  }
  return Object.fromEntries(
    Object.entries(struct).map(([k, v]) => [k, toJson(v)])
  );
}

开始时的四个基本检查可以重构为array.includes()或类似的代码,但是TypeScript编译器将不了解发生了什么,您将需要return struct as Json。我得到它的方式更冗长,但编译器100%确定struct是该块中的有效Json。两种方法都行。

其余的检查只是使用内置的JavaScript函数和方法,如Array.from()Array.prototype.map()Object.entries()Object.fromEntries()

至于类型,如果需要,您可以给toJson()一个调用签名,它试图将输入类型实际映射到输出类型。例如:

代码语言:javascript
复制
type ToJson<T> =
  Struct extends T ? Json :
  Json extends T ? T :
  T extends Map<infer K, infer S> ? { [P in Extract<K, string>]: ToJson<S> } :
  T extends Set<infer S> ? ToJson<S>[] :
  T extends object ? { [K in keyof T]: ToJson<T[K]> } :
  T;

function toJson<T extends Struct>(struct: T): ToJson<T>;

ToJson<T>类似于实现,但表示为类型操作。如果输入类型T只是Struct,那么输出Json。如果它已经是Json的一个子类型,那么输出T。如果是Map<K, S>,那么生成一个对象,其键为K,其值为ToJson<S>。如果是Set<S>,则输出元素为ToJson<S>的数组。如果它是一个对象(包括数组),那么通过ToJson映射所有条目。最后,如果它只是一个基元类型,则返回原语类型。

让我们看看这一切是否有效:

代码语言:javascript
复制
const foo = {
  str: "hello",
  num: 123,
  boo: true,
  nul: null,
  jArr: ["one", 2, { x: 3 }],
  jObj: { a: "one", b: 2, c: { x: 3 } },
  set: new Set([1, 2, 3]),
  map: new Map([["a", 1], ["b", 2]])
}

const json = toJson(foo);

编译器看到的类型如下:

代码语言:javascript
复制
/* const json: {
    str: string;
    num: number;
    boo: boolean;
    nul: null;
    jArr: (string | number | {
        x: number;
    })[];
    jObj: {
        a: string;
        b: number;
        c: {
            x: number;
        };
    };
    set: number[];
    map: {
        [x: string]: number;
    };
} */

注意如何转换setmap属性。让我们确保实现也有效:

代码语言:javascript
复制
console.log(json);
/* {
  "str": "hello",
  "num": 123,
  "boo": true,
  "nul": null,
  "jArr": [
    "one",
    2,
    {
      "x": 3
    }
  ],
  "jObj": {
    "a": "one",
    "b": 2,
    "c": {
      "x": 3
    }
  },
  "set": [
    1,
    2,
    3
  ],
  "map": {
    "a": 1,
    "b": 2
  }
} */

也不错。编译器类型与实现值一致的事实意味着,如果您愿意,可以对其执行其他操作,并获得一些类型检查和提示:

代码语言:javascript
复制
console.log(json.set[0].toFixed(2)); // 1.00

操场链接到代码

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/66673423

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档