我不确定任何 Internet 点是否值得为解决任意嵌套属性的问题而付出努力。为了以编程方式执行此操作,您需要您的函数接受从一种类型到另一种类型的“映射”模式。例如,给定您的类型 A 和 type 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);
这里的AccountA 到AccountB 映射器保留名为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),所以我不得不使用一种解决方法。
那里可能有各种的边缘情况,当涉及到其属性是基元和其他对象类型的联合的对象类型时,或者当属性名称冲突时等等。像这样的东西需要在您考虑在任何类型的生产环境中使用它之前,在类型系统和运行时都进行了大量测试。
因此,您可能会考虑实际用例的范围以及您是否真的想要一些通用的重映射器,或者是否更适合硬编码的方法。对于像A 和B 这样的一对类型,即使没有边缘情况,上述实现也可能是矫枉过正。对于您的实际用例?只有你能说。
Playground link to code