打字稿,合并对象类型? [英] Typescript, merge object types?
问题描述
是否可以合并两种通用对象类型的道具? 我有类似的功能:
Is it possible to merge the props of two generic object types? I have a function similar to this:
function foo<A extends object, B extends object>(a: A, b: B) {
return Object.assign({}, a, b);
}
我希望类型是A中B中不存在的所有属性,以及B中所有的属性.
I would like the type to be all the properties in A that does not exist in B, and all properties in B.
merge({a: 42}, {b: "foo", a: "bar"});
给出了{a: number} & {b: string, a: string}
的一种很奇怪的类型,尽管a
是一个字符串.
实际的返回值给出了正确的类型,但是我不知道如何显式编写它.
gives a rather odd type of {a: number} & {b: string, a: string}
, a
is a string though.
The actual return gives the correct type, but I can not figure how I would explicitly write it.
推荐答案
由近似值不能正确表示如果后面的参数具有与前面的参数同名的属性,将会发生什么.但是直到最近,这还是您在TypeScript的类型系统中可以做的最好的事情.
The intersection type produced by the TypeScript standard library definition of Object.assign()
is an approximation that doesn't properly represent what happens if a later argument has a property with the same name as an earlier argument. Until very recently, though, this was the best you could do in TypeScript's type system.
从引入条件类型但是,在TypeScript 2.8中,您可以找到更接近的近似值.此类改进之一是使用在此处定义的类型函数Spread<L,R>
,像这样:
Starting with the introduction of conditional types in TypeScript 2.8, however, there are closer approximations available to you. One such improvement is to use the type function Spread<L,R>
defined here, like this:
// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
{ [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];
// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
{ [P in K]: L[P] | Exclude<R[P], undefined> };
type Id<T> = {[K in keyof T]: T[K]} // see note at bottom*
// Type of { ...L, ...R }
type Spread<L, R> = Id<
// Properties in L that don't exist in R
& Pick<L, Exclude<keyof L, keyof R>>
// Properties in R with types that exclude undefined
& Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
// Properties in R, with types that include undefined, that don't exist in L
& Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
// Properties in R, with types that include undefined, that exist in L
& SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;
(我对链接定义进行了一些更改;使用标准库中的Exclude
而不是Diff
,并将Spread
类型包装为no-op Id
类型,以使被检查的类型更易于处理而不是一堆交叉路口.
(I've changed the linked definitions slightly; using Exclude
from the standard library instead of Diff
, and wrapping the Spread
type with the no-op Id
type to make the inspected type more tractable than a bunch of intersections).
让我们尝试一下:
function merge<A extends object, B extends object>(a: A, b: B) {
return Object.assign({}, a, b) as Spread<A, B>;
}
const merged = merge({ a: 42 }, { b: "foo", a: "bar" });
// {a: string; b: string;} as desired
您可以看到输出中的a
现在已被正确识别为string
而不是string & number
.是的!
You can see that a
in the output is now correctly recognized as a string
instead of string & number
. Yay!
但是请注意,这仍然是一个近似值:
But note that this is still an approximation:
-
Object.assign()
仅复制可枚举的自有财产,并且类型系统不提供任何方式来表示要过滤的属性的可枚举性和所有权.这意味着merge({},new Date())
看起来就像TypeScript的Date
类型,即使在运行时没有任何Date
方法将被复制并且输出实质上是{}
.这是目前的硬限制.
Object.assign()
only copies enumerable, own properties, and the type system doesn't give you any way to represent the enumerability and ownership of a property to filter on. Meaning thatmerge({},new Date())
will look like typeDate
to TypeScript, even though at runtime none of theDate
methods will be copied over and the output is essentially{}
. This is a hard limit for now.
此外,Spread
的定义实际上不是区别在 missing 属性和存在未定义值的属性之间.因此,当merge({ a: 42}, {a: undefined})
应该为{a: undefined}
时,错误地将其键入为{a: number}
.可以通过重新定义Spread
来解决此问题,但我不确定100%.对于大多数用户而言,它可能不是必需的. (可以通过重新定义type OptionalPropertyNames<T> =
{ [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T]
来解决)
Additionally, the definition of Spread
doesn't really distinguish between missing properties and a property that is present with an undefined value. So merge({ a: 42}, {a: undefined})
is erroneously typed as {a: number}
when it should be {a: undefined}
. This can probably be fixed by redefining Spread
, but I'm not 100% sure. And it might not be necessary for most users. ( this can be fixed by redefining type OptionalPropertyNames<T> =
{ [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T]
)
类型系统无法对其未知的属性执行任何操作. declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows);
在编译时的输出类型为{a: number}
,但是如果whoKnows
恰好是{a: "bar"}
(可分配给{}
),则notGreat.a
在运行时为字符串,但为数字编译时间.哎呀.
The type system can't do anything with properties it doesn't know about. declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows);
will have an output type of {a: number}
at compile time, but if whoKnows
happens to be {a: "bar"}
(which is assignable to {}
), then notGreat.a
is a string at runtime but a number at compile time. Oops.
因此被警告;将Object.assign()
键入为相交或Spread<>
是一种尽力而为"的事情,在边缘情况下可能会使您误入歧途.
So be warned; the typing of Object.assign()
as an intersection or a Spread<>
is kind of a "best-effort" thing, and can lead you astray in edge cases.
无论如何,希望能有所帮助.祝你好运!
Anyway, hope that helps. Good luck!
*注:有人从身份映射类型将Id<T>
的定义编辑为T
.确切地说,这样的更改是不正确的,但是它违背了目的,即迭代键以消除交集.比较:
*Note: Someone edited the definition of Id<T>
from a identity mapped type to be just T
. Such a change isn't incorrect, exactly, but it defeats the purpose... which is to iterate through the keys to eliminate intersections. Compare:
type Id<T> = { [K in keyof T]: T[K] }
type Foo = { a: string } & { b: number };
type IdFoo = Id<Foo>; // {a: string, b: number }
如果检查IdFoo
,您将看到相交已被消除,并且两个成分已合并为一种类型.同样,在可分配性方面,Foo
和IdFoo
之间没有真正的区别.只是后者在某些情况下更易于阅读.诚然,有时编译器的类型的字符串表示形式将只是不透明的Id<Foo>
,因此并不完美.但这确实有目的.如果要在自己的代码中用T
替换Id<T>
,请成为我的客人.
If you inspect IdFoo
you will see that the intersection has been eliminated and the two constituents have been merged into a single type. Again, there's no real difference between Foo
and IdFoo
in terms of assignability; it's just that the latter is easier to read in some circumstances. Admittedly, sometimes the compiler's string representation of a type will just be the opaque-ish Id<Foo>
, so it's not perfect. But it did have a purpose. If you want to replace Id<T>
with T
in your own code, be my guest.
这篇关于打字稿,合并对象类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!