我有以下情况。
function foo<A extends object, B extends A>(
a: A,
b: Pick<B, Exclude<keyof B, keyof A>>
): B {
return undefined as any;
}
const r = foo<{ a: string }, { a: string; b: number }>({ a: "" }, { b: 2 });
我希望b
是一个只有不在a
中的键的对象。但是我还想在将它们合并在一起时获取结果对象的类型。
这可以通过在键上使用Exclude
,然后在这些过滤键上使用Pick
创建最终对象类型来实现:
function foo<A extends object, B extends A>(
a: A,
b: Pick<B, Exclude<keyof B, keyof A>>,
): B {
return undefined as any;
}
只是为了多解释一下:
type A = { a: string };
type B = { a: string; b: number };
// "b"
type FilteredProps = Exclude<keyof B, keyof A>;
// { b: number; }
type FinalType = Pick<B, FilteredProps>;
Exclude<B, Pick<B, keyof A>>
不工作的原因是:
// { a: string; }
type PickedObject = Pick<B, keyof A>;
// never, because { a: string; b: number; } extends { a: string; }
type FinalType = Exclude<B, PickedObject>;
// for reference...
type Exclude<T, U> = T extends U ? never : T;
我认为@DavidSherret已经回答了你提出的问题,但我会谨慎使用任何无法从参数推断类型参数的泛型函数。 在您的情况下,TypeScript 无法真正从b
参数推断出B
,因此您需要在调用foo()
时自己显式指定类型参数。 如果这是你真正想要的,那就太好了。 否则,请继续阅读:
使 TypeScript 推断类型参数A
和B
的最简单方法是使输入参数分别a
和类型为A
和B
的b
。 例如:
declare function foo<A, B>(a: A, b: B): A & B;
执行此操作时,这将按预期运行:
const r = foo({ a: "" }, { b: 2 });
// A is inferred as {a: string}
// B is inferred as {b: number}
// output is therefore {a: string} & {b: number}
交集类型输出等效于{a: string, b: number}
。 如果你确实需要它完全{a: string, b: number}
而不是等效,你可以使用映射类型来做到这一点:
type Id<T> = { [K in keyof T]: T[K] };
declare function foo<A, B>(a: A, b: B): Id<A & B>;
const r = foo({ a: "" }, { b: 2 });
// A is inferred as {a: string}
// B is inferred as {b: number}
// output is {a: string, b: number}
现在你可能会抱怨这并不能阻止你b
与a
有重叠的键,这就是你问题的重点。
const bad = foo({ a: "" }, { a: 3 }); // not an error!!
因此,让我们解决这个问题:
function foo<A, B extends { [K in keyof B]: K extends keyof A ? never : B[K] }>(
a: A, b: B
): Id<A & B> {
return Object.assign({}, a, b); // implementation, why not
}
现在我们已经将B
限制为{ [K in keyof B]: K extends keyof A ? never : B[K]}
,这是...东西。 让我们把它拆开。 它与B
([K in keyof B]
)具有相同的键,对于每个键,如果键是keyof A
的一部分,则值为never
。 否则(如果键不是keyof A
的一部分),则值与B
中的值相同。 所以它基本上是说B
必须被约束为一种类型,其中任何与A
重叠的键都必须具有一种never
。 例如,如果A
是{a: string}
,而B
是{a: number, b: boolean}
,则该映射类型变为{a: never, b: boolean}
。 由于{a: number, b: boolean}
不扩展{a: never, b: boolean}
,这将失败。 也许这解释太多了。 让我们看看它的实际效果:
const r = foo({ a: "" }, { b: 2 }); // {a: string, b: number}
这仍然可以按预期工作。 但以下内容失败并显示错误
const bad = foo({ a: "" }, { a: 3 }); // error in second argument
// types of property 'a' are incompatible.
// 'number' is not assignable to type 'never'
这就是你想要的。 因此,您可以获得与预期函数相同的行为,以及类型参数推断! 好的,希望有帮助。 祝你好运。