【问题标题】:How to properly type an Object.assign-like generic function?如何正确键入类似 Object.assign 的泛型函​​数?
【发布时间】:2022-01-25 14:59:41
【问题描述】:

我正在尝试在 TypeScript 中创建一个“分配默认值”函数,它循环遍历 source 的键,如果同一键的值在 target 中为空,它将使用该值从source 代替。这是我的尝试:

const assignDefault = <T, U>(target: T, source: U): T & U => {
  Object.keys(source).forEach(key => {
    // typecasted as Object.keys returns string[]
    const prop = target[key as keyof T]
    if (typeof prop === 'undefined' || prop === null) {
      // Error: Type 'U[keyof U]' is not assignable to type 'T[keyof T]'.
      target[key as keyof T] = source[key as keyof U] 
    }
  })
  return target // Error: Type 'T' is not assignable to type 'T & U'.
}

我从 Object.assign 在 TypeScript 中的输入中借用了泛型:

ObjectConstructor.assign<T, U>(target: T, source: U): T & U;

但我找不到解决这些错误的方法。

Playground

【问题讨论】:

  • 我认为正确反映您的意图的函数类型应该是&lt;T, U = Partial&lt;T&gt;&gt;(target: T, source: U): T,因为您不想添加任何不属于T 的新属性?见Partial type

标签: typescript typescript-generics


【解决方案1】:

我从 Object.assign 的类型中借用了泛型

类型定义和实现之间有很大的不同。编译器对标准库中的: T &amp; U 很满意,因为没有冲突的信息。但是,在您的实现中,target 的类型只是 T,而签名需要 T &amp; U,因此会出现编译器错误。

编译器不是运行时,所以虽然它做了一些控制流分析,但它不能推断出forEach 变异了target。不过,有一些方法可以让编译器意识到这一点。最简单的一个是returned 值的asserting the type,可以是any(禁用它的类型检查器)或T &amp; U

const assignDefault = <T, U>(target: T, source: U): T & U => {
    Object.keys(source).forEach((key) => {
        const prop = target[key as keyof T]
        if (typeof prop === 'undefined' || prop === null) {
          // Error: Type 'U[keyof U]' is not assignable to type 'T[keyof T]'.
          target[key as keyof T] = source[key as keyof U] 
        }
    });
    return target as T & U;
};

Playground

但是,assignDefault 的返回类型还有另一个问题:T &amp; U 是一个交集,这不是您想要的。要正确键入“如果为空,则分配”,需要一个 mapped type 和一些 conditional typesextends 检查以确定是否可以分配属性。

这样的类型可能看起来像这样:

type AssignIfNullish<T, U> = {
    [P in keyof T]:  
        T[P] extends null | undefined ? // is value a subtype of null|undefined?
        U[P & keyof U]: // take the value from U
        T[P] // preserve the original
};

// type Test = { a: "null"; b: "undefined"; c: 42; }
type Test = AssignIfNullish<
    { a: null,  b: undefined,  c: 42 }, 
    { a:"null", b:"undefined", c: 24 }
>;

Playground

剩下的只是断言函数的返回类型:

const assignDefault = <T, U>(target: T, source: U) => {
    Object.keys(source).forEach((key) => {
        const prop = target[key as keyof T]
        if (typeof prop === 'undefined' || prop === null) {
          // Error: Type 'U[keyof U]' is not assignable to type 'T[keyof T]'.
          target[key as keyof T] = source[key as keyof U] 
        }
    });
    return target as AssignIfNullish<T, U>;
};

assignDefault({ a:1,b:null } as const,{ b:42 } as const); // { a:1, b:42 }

Playground

最后,还有一个错误需要处理:

错误:类型“U[keyof U]”不可分配给类型“T[keyof T]”。

编译器检查U[keyof U] (source[key]) 是否可分配给T[keyof T] (target[key]),但它没有任何关于TU 如何相关的信息。它所知道的是两者都是泛型类型参数,因此可以是任何东西。根据签名,甚至不能保证两者都是对象(知道,但编译器不知道):

assignDefault(true, false); // no objection from the compiler

由于在这种情况下您比编译器了解更多,因此可以使用as unknown as T[keyof T] 断言source[key] 实际上是T[keyof T]

const assignDefault = <
    T extends object,
    U extends object
>(target: T, source: U) => {
    Object.keys(source).forEach((key) => {
        const prop = target[key as keyof T]
        if (typeof prop === 'undefined' || prop === null) {
            target[key as keyof T] = source[key as keyof U] as unknown as T[keyof T];
        }
    });
    return target as AssignIfNullish<T, U>;
};

assignDefault(true, false); // error as expected
assignDefault({ a:null }, { a:42, b: "extra" }); // ok, { a:number }

Playground

但是,这是很多断言,我们可以做得更好吗?是的,如果我们放弃 Object.keys 以支持旧的 for...in 循环,因为 key 被键入为 string 的怪癖(这是有充分理由的,但仍然如此)。使用for...in(带有适当的保护)允许我们删除as keyof 断言:

const assignDefault = <
    T extends Partial<{ [P in keyof U]: unknown }> & object,
    U extends Partial<{ [P in keyof T]: unknown }> & object
>(target: T, source: U) => {
    for (const key in source) {
        if (!Object.prototype.hasOwnProperty.call(source, key)) continue;

        const prop = target[key];

        if (typeof prop === 'undefined' || prop === null) {
            Object.assign(target, key, { [key]: source[key] });
        }
    }

    return target as AssignIfNullish<T, U>;
};

Playground

注意使用Object.assign(target, key, { [key]: source[key] }); 以避免可分配性错误(也可以使用Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)!);)。

【讨论】:

    【解决方案2】:

    这是我想出来的。

    注意几点:

    typeof 永远不会返回 "null" 请参阅:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof

    第二件事 T &amp; U 类型需要目标和源的组合。您可以通过使用扩展运算符合并它们来相当容易地实现这一点,如我的示例所示。

    [编辑] 而不是受到您所需类型的限制。我完成了对功能的修改,更符合你的初衷。

    const assignDefault = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
        for (const key in source) {
            if (target[key] === null || typeof target[key] === 'undefined') {
                target[key] = source[key];
            }
        }
        return target;
    }
    
    const myTarget = {
        prop1: null,
        prop2: "some value",
        prop3: ["some", "other", "value"]
    }
    
    const mySource = {
        prop1: "hello world",
        prop2: "don't show this value",
        prop3: [],
        prop4: "a value not provided by target originally"
    }
    
    // expect
    /**
     * {
     *  prop1: "hello world",
     *  prop2: "some value",
     *  prop3: ["some", "other", "value"],
     *  prop4: "a value not provided by target originally"
     * }
     */
    
    const newObj = assignDefault(myTarget, mySource);
    
    console.log(newObj);
    

    请注意,此函数仅适用于具有键值对的对象。

    【讨论】:

    • 不幸的是,根据我的编译器,这不起作用,这与Type 'T[keyof T] | U[keyof U]' is not assignable to type 'T[keyof T]'. 的问题相同,而且单独的扩展运算符不会将其作为速记,因为它会覆盖非空值。
    • 我认为可能有些混乱。在此示例中,target 的值不会被覆盖。我制作了一个小pastebin,可以更好地演示传播速记。 playground
    • 当然不会覆盖null 的值。如果您想要一个默认情况下覆盖空值和未定义值的示例(就像您最初的问题似乎是有意的那样),请告诉我,我可以更新我的答案。
    • 我决定用可以解决您的问题并忽略您想要的类型的东西来更新我的答案。
    猜你喜欢
    • 2019-07-19
    • 1970-01-01
    • 1970-01-01
    • 2021-10-22
    • 2020-07-25
    • 1970-01-01
    • 2021-06-15
    • 2020-11-27
    • 2021-06-23
    相关资源
    最近更新 更多