【问题标题】:TypeScript: How to convert object back and forth between typesTypeScript:如何在类型之间来回转换对象
【发布时间】:2020-12-08 19:07:56
【问题描述】:

假设我有两种类型

type A = {
  fi_name: string;
  l_name: string;
  f_name: string;
}

type B = {
  firstName: string;
  lastName: string;
  fullName: string;
} 

有没有一种方法可以轻松地在这些类型之间来回转换对象?

额外的互联网点如果答案也处理嵌套类型和/或可以使用 expressjs 中间件实现。

【问题讨论】:

  • 你没有想到一个简单的方法来创建两个映射函数吗?无论如何,您需要一个描述像 fl_name - firstName 这样的对的映射。
  • 我试图避免创建 2 个函数,即 A2B() 和 B2A()。我希望有一个模块或一些构建方式能够简化事情。

标签: javascript node.js typescript express casting


【解决方案1】:

我不确定任何 Internet 点是否值得为解决任意嵌套属性的问题而付出努力。为了以编程方式执行此操作,您需要您的函数接受从一种类型到另一种类型的“映射”模式。例如,给定您的类型 Atype B,您可以这样做:

const aToB = makeMapper<A, B>()({
  f_name: "fullName",
  fi_name: "firstName",
  l_name: "lastName"
} as const);

在这里您可以看到假设的 makeMapper 函数是通用的,它允许您指定所需的输入类型 (A) 和所需的输出类型 (B),然后其输出接受映射模式,其中每个属性名称都映射到另一个属性名称。 (这是一个 curried 函数,因为 TypeScript 缺少类型参数的部分规范;您需要用于输入和输出的类型参数,以及用于映射的类型参数。有关更多信息,请参阅 this answer。)(另外,您需要 const assertion以确保编译器知道 "fullName" 之类的字符串文字值,并且不会将它们扩大到 string)。

如果您在映射中做错了什么,您会看到一些错误消息:

const badMapper = makeMapper<A, B>()({
  f_name: "fullName",
  fi_name: "firstName",
  l_name: "lostName",
} as const); // error!
// 'Invalid<{ lastName: "missing or misspelled property key"; }>'

这里我写的是lostName 而不是lastName,所以我希望上述类型出现错误,告诉我lastName 属性在某种程度上是错误的。

对于对象属性,您需要映射架构不仅接受重命名的键,还接受对象类型的映射器。例如:

interface AccountA {
  user: A
}
interface AccountB {
  user: B
}
const accountAToAccountB = makeMapper<AccountA, AccountB>()({
  user: ["user", aToB]
} as const);

这里的AccountAAccountB 映射器保留名为user 的属性,但使用我们之前创建的aToB 映射器将类型从A 更改为B

然后,实际向前和向后使用生成的映射器将如下所示:

const a: AccountA = {
  user: {
    fi_name: "Harry",
    l_name: "Potter",
    f_name: "Harry Potter"
  }
}

const b: AccountB = accountAToAccountB.map(a);

console.log(b);
/* {
  "user": {
    "firstName": "Harry",
    "lastName": "Potter",
    "fullName": "Harry Potter"
  }
}  */

const aAgain: AccountA = accountAToAccountB.reverseMap(b);
console.log(aAgain);
/* {
  "user": {
    "fi_name": "Harry",
    "l_name": "Potter",
    "f_name": "Harry Potter"
  }
} */

对吗?


好吧,你可以在 TypeScript 中做到这一点,但是打字非常难看而且涉及到,而且实现需要大量的类型断言。以下是如何做到这一点:

class Mapper<T extends object, M extends ObjectMapping<T>> {
  constructor(public mapping: M) { }
  map(obj: T): Mapped<T, M> {
    return Object.fromEntries(Object.entries(obj).map(([k, v]) => {
      if (!(k in this.mapping)) return [k, v];
      const m = this.mapping[k as keyof T];
      if (Array.isArray(m)) return [m[0], m[1] ? m[1].map(v) : v];
      return [m, v];
    })) as any;
  }
  reverseMap(obj: Mapped<T, M>): T {
    const revMapping: Record<string, any> = Object.fromEntries(Object.entries(this.mapping).map(([k, m]) => {
      if (Array.isArray(m)) return [m[0], [k, m[1]]];
      return [m, k];
    }));
    return Object.fromEntries(Object.entries(obj).map(([k, v]) => {
      if (!(k in revMapping)) return [k, v];
      const m = revMapping[k];
      if (Array.isArray(m)) return [m[0], m[1] ? m[1].reverseMap(v) : v];
      return [m, v];
    })) as any;
  }
}

type ObjectMapping<T extends object> = {
  [K in keyof T]: T[K] extends object ? readonly [PropertyKey, Mapper<T[K], any>?] : PropertyKey
}

type Mapped<T extends object, M extends ObjectMapping<T>> = {
  [K in keyof T as GetKey<M[K]>]: GetVal<M[K], T[K]>
} extends infer O ? { [K in keyof O]: O[K] } : never;
type GetKey<P> = P extends PropertyKey ? P : P extends readonly [PropertyKey, any?] ? P[0] : never;
type GetVal<P, V> = P extends PropertyKey ? V :
  P extends readonly [PropertyKey, Mapper<any, any>] ? V extends object ? Mapped<V, P[1]['mapping']> : V : V;

type Invalid<Err> = {
  errMsg: Err
}

function makeMapper<T extends object, U extends object>() {
  return <M extends ObjectMapping<T>>(mapping: (Mapped<T, M> extends U ? M : M & Invalid<
    { [K in keyof U as K extends keyof Mapped<T, M> ? Mapped<T, M>[K] extends U[K] ? never : K : K]:
      K extends keyof Mapped<T, M> ? Mapped<T, M>[K] extends U[K] ? never :
      ["wrong property type", U[K], "vs", Mapped<T, M>[K]] : "missing or misspelled property key" }
  >)) => new Mapper<T, M>(mapping as M)
}

我应该费心解释每个部分是如何工作的吗?打字基本上是对 TS4.1 的key remapping in mapped types 的递归使用。该实现还递归地遍历mapping 架构并将映射应用于输入对象的条目。

让编译器在面对如此复杂的类型时产生有用的错误消息并不简单,因为目前 TypeScript 中没有“抛出/无效”类型(请参阅microsoft/TypeScript#23689),所以我不得不使用一种解决方法。

那里可能有各种的边缘情况,当涉及到其属性是基元和其他对象类型的联合的对象类型时,或者当属性名称冲突时等等。像这样的东西需要在您考虑在任何类型的生产环境中使用它之前,在类型系统和运行时都进行了大量测试。

因此,您可能会考虑实际用例的范围以及您是否真的想要一些通用的重映射器,或者是否更适合硬编码的方法。对于像AB 这样的一对类型,即使没有边缘情况,上述实现也可能是矫枉过正。对于您的实际用例?只有你能说。

Playground link to code

【讨论】:

  • 太棒了!按预期工作,从心底里感谢你!但我认为错误处理没有像你预期的那样工作;正如您在操场错误日志中看到的那样,我们得到[...] Property 'errMsg' is missing in type [...]
  • 它按预期工作,只是丑陋。你必须猜到“errMsg 缺失并且在类型 {errMsg: {someProp: “mispelled or missing prop”} 中是必需的”实际上意味着“你的映射拼写错误或缺少属性“someProp”” . TS 中没有对自定义错误消息的官方支持,因此这是一种解决方法
  • 我明白了。再次感谢。
猜你喜欢
  • 2017-07-28
  • 1970-01-01
  • 2019-06-23
  • 2020-12-28
  • 2021-11-04
  • 2018-07-14
  • 1970-01-01
  • 2014-05-17
相关资源
最近更新 更多