【问题标题】:Typescript function merge object at a specific pathTypescript函数在特定路径合并对象
【发布时间】:2020-06-16 05:32:48
【问题描述】:

我有一个合并功能,可以合并特定路径的对象。

const merge = (src, path, newObj) => {
   // this code works fine
}

要使用这个函数,我这样称呼它:

interface IUser {
   user: {
      address: {
        street: string
      }
   }
}

interface IAddrNum {
   door: number
}

const User: IUser = {
   user: {
     address: {
        street: "New Street"
     }
   }
}


const mergeObj: IAddrNum = {
   door: 59
}

const newObj = merge(User, "user.address", mergeObj);

这样,我得到了正确的结果。

{
   user: {
     address: {
       street: "New Street"
       door: 59
     }
   }
}

问题:我想在 typescript 中为这个函数创建一个签名。

interface Immutable {
   merge<T, K, M>(
     src: T,
     path: K,
     merge: M
   ): T & M // <== this is where the problem is
}

这没有按预期工作。它不能是T &amp; K,因为合并必须发生在特定路径上。是否有可能拥有此功能的签名?如果是这样,你能给我一些方向吗?谢谢。

【问题讨论】:

  • 你不能用"user.address" 来做,因为编译器不能在类型级别解析字符串。相反,您甚至需要使用 ["user", "address"] as const 形式的元组之类的东西才能开始编写它。或者您可以将merge 参数设为自己的对象,例如{user: { address: {door: 59}}},而根本不提及路径(在这种情况下,返回类型看起来确实有点像T &amp; M)。我很乐意就这些选项中的任何一个提供建议,但点路径字符串文字不是首发。
  • 谢谢@jcalz。我不确定"user.address" 是否有效。谢谢你的解释。我可以做到["user","address"]。如果您能指导我进行这项新更改,那就太好了。

标签: javascript typescript typescript-generics


【解决方案1】:

如果您可以使用路径元组,那么我们需要操作元组。一个有用的类型别名是Tail&lt;T&gt;,它采用像[string, number, boolean] 这样的元组类型并返回另一个删除了第一个元素的元组,比如[number, boolean]

type Tail<T extends any[]> = 
  ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;

然后我们可以写DeepRecord&lt;K, V&gt;,其中K是属性键的路径,V是一些值。我们创建了一个递归映射条件类型,它产生一个对象类型,其中V 位于K 描述的路径中:

type DeepRecord<K extends PropertyKey[], V> =
    K extends [] ? V : { [P in K[0]]: DeepRecord<Tail<K>, V> };

只是为了确保它有效,这里有一个例子"

type Example = DeepRecord<["foo", "bar", "baz"], string>;
/* type Example = { foo: { bar: { baz: string; }; };} */

现在我们基本上已经完成了,但是创建一个递归合并交叉点的东西也很好,这样你就可以得到一个 {foo: {bar: {baz: string; qux: number;}}} 而不是 {foo: {bar: {baz: string; qux: number;}}}

type MergeIntersection<T> = 
  T extends object ? { [K in keyof T]: MergeIntersection<T[K]> } : T;

最后,我们可以给merge() 一个类型签名(以及一个实现,尽管这超出了问题的范围并且不保证是正确的):

const merge = <T extends object, K extends N[] | [], 
  V extends object, N extends PropertyKey>(
    src: T, path: K, newObj: V
) => {
    const ret = { ...src } as MergeIntersection<T & DeepRecord<K, V>>;
    let obj: any = ret;
    for (let k of path) {
        if (!(k in obj)) {
            obj[k] = {};
        }
        obj = obj[k];
    }
    Object.assign(obj, newObj);
    return ret;
}

请注意,N 在定义中的作用并不大,但它有助于让编译器推断出path 参数包含literals 而不仅仅是stringK 约束中的 | [] 有助于编译器推断元组而不是数组。您希望 ["user", "address"] 被推断为 ["user", "address"] 而不是 string[] 类型,否则整个事情就会崩溃。这个烦人的魔法是microsoft/TypeScript#30680 的主题,目前这是我能做的最好的。

您可以在示例代码上对其进行测试:

const newObj = merge(User, ["user", "address"], mergeObj);
/* const newObj: {
    user: {
        address: {
            street: string;
            door: number;
        };
    };
}*/

console.log(JSON.stringify(newObj));
// {"user":{"address":{"street":"New Street","door":59}}}

我认为看起来不错。好的,希望有帮助;祝你好运!

Playground link to code

【讨论】:

  • 感谢@jcalz 的精彩解释并将其分解为我理解。这非常有效。
  • 根据您的代码,我正在尝试为del 编写一个类型。在这种情况下,它将是del(User, ["user","address","street"])。你能指导我吗?我已经修改了DeepRecord 来创建对象,但我无法从源对象中减去它。
  • 这是一个不同的问题,不是吗?我很乐意看它,但不是在 cmets 部分(无论如何,您可能想发布一个新问题以引起其他人的注意,以防我无法解决)
  • 当然,这是我刚刚发布的链接-stackoverflow.com/questions/60529460/…
猜你喜欢
  • 2023-01-12
  • 1970-01-01
  • 2020-04-14
  • 2016-09-08
  • 2021-05-05
  • 1970-01-01
  • 2022-12-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多