我从 Object.assign 的类型中借用了泛型
类型定义和实现之间有很大的不同。编译器对标准库中的: T & U 很满意,因为没有冲突的信息。但是,在您的实现中,target 的类型只是 T,而签名需要 T & U,因此会出现编译器错误。
编译器不是运行时,所以虽然它做了一些控制流分析,但它不能推断出forEach 变异了target。不过,有一些方法可以让编译器意识到这一点。最简单的一个是returned 值的asserting the type,可以是any(禁用它的类型检查器)或T & 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 & U 是一个交集,这不是您想要的。要正确键入“如果为空,则分配”,需要一个 mapped type 和一些 conditional types 和 extends 检查以确定是否可以分配属性。
这样的类型可能看起来像这样:
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]),但它没有任何关于T 和U 如何相关的信息。它所知道的是两者都是泛型类型参数,因此可以是任何东西。根据签名,甚至不能保证两者都是对象(你知道,但编译器不知道):
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)!);)。