typescript 用不同类型的值替换JSON中深度嵌套的值,同时保持类型安全?

vatpfxk5  于 2022-12-24  发布在  TypeScript
关注(0)|答案(1)|浏览(170)

我编写了一个函数,它将JSON对象和定义要替换的值的Map作为输入;并返回相同的JSON对象,其中所有出现的值都被相应的替换项替换--替换项可以是任何值。
这将更改对象的类型,但我不知道如何在TypeScript中反映此更改。
函数如下:

function replaceJsonValues<Source, Replacement, Output>(
  obj: Source,
  translatedKeyData: Map<string, Replacement>
): Output {
  let stringifiedObject = JSON.stringify(obj);
  for (const [key, keyProp] of translatedKeyData.entries()) {
    stringifiedObject = stringifiedObject.replaceAll(`"${key}"`, JSON.stringify(keyProp));
  }
  return JSON.parse(stringifiedObject);
}

type SourceType = {
  foo: string; 
  baz: { 
    test: string;
  } 
}[]

type ReplacementType = {
  fancy: string;
}

const source: SourceType = [{ foo: "bar", baz: { test: "bar" } }];
const replacement: ReplacementType = { fancy: "replacement" };

const result = replaceJsonValues(source, new Map([["bar", replacement]]));
//    ^?

console.log(result)

在TSPlayground见。
如何修改它以使Output类型正确?

wztqucjr

wztqucjr1#

有几个问题需要解决:

  • 我假设您关心translatedKeyData中键/值对之间的特定关系,如果是这样,您就不能轻松地使用Map来完成此操作。在TypeScript中,Map的类型为Map<K, V>,这是一种类似于记录的类型,其中所有键都被视为彼此相同的类型。并且所有的值都被认为是彼此相同的类型...特定键和特定值之间的任何关联都将丢失。有一些方法可以更改Map的类型,以便它确实跟踪此类连接,如Typescript: How can I make entries in an ES6 Map based on an object key/value type中所述,但是使用普通对象开始会容易得多。您可以使用{k1: v1, k2: v2, ...}代替new Map([["k1", v1], ["k2", v2], ...])。您可以使用the Object.entries() method代替the entries() method of Map
  • 我强烈建议不要对JSON字符串执行字符串操作,因为这样很容易意外地产生无效的JSON。
replaceJsonValues({ a: "b", c: "d" }, new Map([[",", "oops"]])); 
// 💥 RUNTIME ERROR! parsing "{"a":"b"oops"c":"d"}"

例如,因为您以JavaScript值开始和结束,所以您可以通过遍历对象本身来执行概念上相同的转换,这样您就可以确信您只替换了字符串值的键或字符串值,而不是奇怪的东西。

function replaceValues(obj: any, translatedKeyData: any) {
  if (typeof obj === "string") {
    return (obj in translatedKeyData) ? translatedKeyData[obj] : obj;
  }
  if (Array.isArray(obj)) {
    return obj.map(v => replaceValues(v, translatedKeyData));
  }
  if (obj && typeof obj === "object") {
    return Object.fromEntries(Object.entries(obj).map(([k, v]) => [
      ((k in translatedKeyData) &&
        typeof translatedKeyData[k] === "string"
      ) ? translatedKeyData[k] : k,
      replaceValues(v, translatedKeyData)]
    ));
  }
  return obj;
}
  • 最后,只有当编译器知道源代码obj和MaptranslatedKeyData上要替换的所有字符串值的文本类型时,这才有可能起作用。那么替换将不会被正确地键入。这意味着你需要在编译时知道源代码和Map的细节。如果它们只在运行时是已知的,那么你的选择将非常有限,我将假设你有编译时已知的值,并且这些值将用constAssert初始化,以向编译器给予最大的信息。

好了,现在输入:我会这样做:

function replaceValues<S, M extends object>(
  obj: S,
  translatedKeyData: M
): ReplaceValues<S, M>;

type ReplaceValues<S, M extends object> =
  S extends keyof M ? M[S] :
  S extends readonly any[] ? {
    [I in keyof S]: ReplaceValues<S[I], M>
  } :
  S extends object ? {
    [K in keyof S as (
      K extends keyof M ? M[K] extends string ? M[K] : K : K
    )]:
    ReplaceValues<S[K], M>
  } :
  S;

所以源对象是generic类型S,Map对象是泛型类型M,然后返回类型是ReplaceValues<S, M>,这是一个递归的conditionalt类型,它遍历S的不同情况并相应地执行替换。
首先:S extends keyof M ? M[S]意味着如果源代码SM中的一个键,那么你可以用M中对应的属性M[S]替换S,这是直接的字符串值替换:实现中typeof obj === "string"代码块的类型级别版本。
那么:S extends readonly any[] ? { [I in keyof S]: ReplaceValues<S[I], M> } :意味着如果源代码S是一个类数组类型,那么我们将这个类数组类型Map到另一个类数组类型,其中每个值都被递归地替换,这是实现中Array.isArray(obj)代码块的类型级别版本。
那么:S extends object ? { [K in keyof S as ( K extends keyof M ? M[K] extends string ? M[K] : K : K )]: ReplaceValues<S[K], M> } :意味着如果源S是一个非数组对象,那么我们Map对象的键和值,这样在M中找到的任何键都被重新Map,同时递归地将ReplaceValues应用于每个值类型。
最后,如果所有这些都是false,那么我们返回S。这是从底部掉下去的,所以数字还是数字,布尔还是布尔,等等。这是return obj在实现底部的类型级别版本。
好吧,让我们看看它是否有效:

const source = [{ foo: "bar", baz: { test: "bar" }, qux: 123 }] as const;
const replacement: ReplacementType = { fancy: "replacement" };

const result = replaceValues(source, { bar: replacement, qux: "foop" } as const);
/* const result: readonly [{
    readonly foo: ReplacementType;
    readonly baz: {
        readonly test: ReplacementType;
    };
    readonly foop: 123;
}] */

看起来不错!编译器将"bar"值替换为ReplacementType,将"qux"键替换为"foop",这与运行时实际生成的对象完全一致:

console.log(result);
/* [{
  "foo": {
    "fancy": "replacement"
  },
  "baz": {
    "test": {
      "fancy": "replacement"
    }
  },
  "foop": 123
}] */

Playground代码链接

相关问题