简体   繁体   English

Typescript,合并object类型?

[英]Typescript, merge object types?

Is it possible to merge the props of two generic object types?是否可以合并两个通用 object 类型的道具? I have a function similar to this:我有一个类似于此的 function:

function foo<A extends object, B extends object>(a: A, b: B) {
    return Object.assign({}, a, b);
}

I would like the type to be all the properties in A that does not exist in B, and all properties in B.我希望类型是 A 中不存在于 B 中的所有属性,以及 B 中的所有属性。

merge({a: 42}, {b: "foo", a: "bar"});

gives a rather odd type of {a: number} & {b: string, a: string} , a is a string though.给出了一个相当奇怪的类型{a: number} & {b: string, a: string} ,虽然a是一个字符串。 The actual return gives the correct type, but I can not figure how I would explicitly write it.实际返回给出了正确的类型,但我不知道我将如何明确地写它。

UPDATE for TS4.1+ TS4.1+ 更新

The original answer still works (and you should read it if you need an explanation), but now that recursive conditional types are supported, we can write merge() with to be variadic:原始答案仍然有效(如果需要解释,应该阅读它),但是现在支持递归条件类型,我们可以将merge()编写为可变参数:

type OptionalPropertyNames<T> =
  { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T];

type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never

type SpreadTwo<L, R> = Id<
  & Pick<L, Exclude<keyof L, keyof R>>
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;

type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ?
  SpreadTwo<L, Spread<R>> : unknown

type Foo = Spread<[{ a: string }, { a?: number }]>

function merge<A extends object[]>(...a: [...A]) {
  return Object.assign({}, ...a) as Spread<A>;
}

And you can test it:你可以测试它:

const merged = merge(
  { a: 42 },
  { b: "foo", a: "bar" },
  { c: true, b: 123 }
);
/* const merged: {
    a: string;
    b: number;
    c: boolean;
} */

Playground link to code Playground 链接到代码

ORIGINAL ANSWER原答案


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. Object.assign()TypeScript 标准库定义产生的交集类型是一个近似值,它不能正确表示如果后面的参数具有与前面的参数同名的属性会发生什么。 Until very recently, though, this was the best you could do in TypeScript's type system.不过,直到最近,这是您在 TypeScript 的类型系统中所能做的最好的事情。

Starting with the introduction of conditional types in TypeScript 2.8, however, there are closer approximations available to you.但是,从 TypeScript 2.8 中条件类型的引入开始,您可以使用更接近的近似值。 One such improvement is to use the type function Spread<L,R> defined here , like this:其中一项改进是使用此处定义的类型函数Spread<L,R> ,如下所示:

// 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> = T extends infer U ? { [K in keyof U]: U[K] } : never // 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>
  >;

(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). (我稍微改变了链接的定义;使用标准库中的Exclude而不是Diff ,并用无操作Id类型包装Spread类型,使检查的类型比一堆交集更易于处理)。

Let's try it out:让我们试试看:

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

You can see that a in the output is now correctly recognized as a string instead of string & number .你可以看到a在输出现在可以正确识别为一个string ,而不是string & number Yay!好极了!


But note that this is still an approximation:但请注意,这仍然是一个近似值:

  • 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. Object.assign()只复制可枚举的、自己的属性,类型系统没有给你任何方式来表示要过滤的属性的可枚举性和所有权。 Meaning that merge({},new Date()) will look like type Date to TypeScript, even though at runtime none of the Date methods will be copied over and the output is essentially {} .这意味着merge({},new Date())看起来像 TypeScript 的Date类型,即使在运行时不会复制任何Date方法并且输出本质上是{} This is a hard limit for now.目前这是一个硬性限制。

  • Additionally, the definition of Spread doesn't really distinguish between missing properties and a property that is present with an undefined value .此外, Spread的定义并没有真正区分缺失的属性和存在未定义值的属性。 So merge({ a: 42}, {a: undefined}) is erroneously typed as {a: number} when it should be {a: undefined} .所以merge({ a: 42}, {a: undefined})被错误地输入为{a: number}而应该是{a: undefined} This can probably be fixed by redefining Spread , but I'm not 100% sure.这可能可以通过重新定义Spread来解决,但我不是 100% 确定。 And it might not be necessary for most users.对于大多数用户来说,它可能不是必需的。 (Edit: this can be fixed by redefining type OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T] ) (编辑:这可以通过重新定义type OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T] )

  • 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.在编译时将有一个{a: number}的输出类型,但如果whoKnows恰好是{a: "bar"} (可分配给{} ),那么notGreat.a在运行时是一个字符串,但在编译时间。 Oops.哎呀。

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.Object.assign()输入为一个交集或一个Spread<>是一种“尽力而为”的事情,并且可能会导致您在极端情况下误入歧途。

Playground link to code Playground 链接到代码


*Note: Id<T> is an identity type and in principle shouldn't do anything to the type. *注意: Id<T>是一种身份类型,原则上不应对该类型做任何事情。 Someone at some point edited this answer to remove it and replace with just T .有人在某个时候编辑了这个答案以将其删除并仅替换为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> = T extends infer U ? { [K in keyof U]: U[K] } : never 

type Foo = { a: string } & { b: number };
type IdFoo = Id<Foo>; // {a: string, b: number }

If you inspect IdFoo you will see that the intersection has been eliminated and the two constituents have been merged into a single type.如果您检查IdFoo您将看到交集已被消除并且两个组成部分已合并为一个类型。 Again, there's no real difference between Foo and IdFoo in terms of assignability;同样, IdFoo分配性而言, FooIdFoo之间没有真正的区别。 it's just that the latter is easier to read in some circumstances.只是后者在某些情况下更容易阅读。

I think you're looking for more of a union ( | ) type instead of an intersection ( & ) type.我认为您正在寻找更多的联合( | ) 类型而不是交叉 ( & ) 类型。 It's closer to what you want...它更接近你想要的......

function merge<A, B>(a: A, b: B): A | B {
  return Object.assign({}, a, b)
}

merge({ a: "string" }, { a: 1 }).a // string | number
merge({ a: "string" }, { a: "1" }).a // string

learning TS I spent a lot of time coming back tothis page ... it's a good read (if you're into that sort of thing) and gives a lot of useful information学习 TS 我花了很多时间回到这个页面......这是一个很好的阅读(如果你喜欢那种东西)并且提供了很多有用的信息

If you want to preserve property order, use the following solution.如果要保留属性顺序,请使用以下解决方案。

See it in action here .这里查看它的实际效果。

export type Spread<L extends object, R extends object> = Id<
  // Merge the properties of L and R into a partial (preserving order).
  Partial<{ [P in keyof (L & R)]: SpreadProp<L, R, P> }> &
    // Restore any required L-exclusive properties.
    Pick<L, Exclude<keyof L, keyof R>> &
    // Restore any required R properties.
    Pick<R, RequiredProps<R>>
>

/** Merge a property from `R` to `L` like the spread operator. */
type SpreadProp<
  L extends object,
  R extends object,
  P extends keyof (L & R)
> = P extends keyof R
  ? (undefined extends R[P] ? L[Extract<P, keyof L>] | R[P] : R[P])
  : L[Extract<P, keyof L>]

/** Property names that are always defined */
type RequiredProps<T extends object> = {
  [P in keyof T]-?: undefined extends T[P] ? never : P
}[keyof T]

/** Eliminate intersections */
type Id<T> = { [P in keyof T]: T[P] }

我找到了一种语法来声明一种合并任何两个对象的所有属性的类型。

type Merge<A, B> = { [K in keyof (A | B)]: K extends keyof B ? B[K] : A[K] };

tl;dr tl;博士

type Expand<T> = T extends object
  ? T extends infer O
    ? { [K in keyof O]: O[K] }
    : never
  : T;

type UnionToIntersection<U> = Expand<
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never
>;

const merge = <A extends object[]>(...a: [...A]) => {
  return Object.assign({}, ...a) as UnionToIntersection<A[number]>;
};

Motivation for yet another answer另一个答案的动机

jcalz answer is good and worked for me for years. jcalz 的回答很好,并且为我工作了很多年。 Unfortunately, as the count of merged objects grows to a certain number, typescript produces the following error:不幸的是,随着合并对象的数量增长到一定数量,typescript 会产生以下错误:

Type instantiation is excessively deep and possibly infinite. [2589]

and fails to deduce the resulting object type.并且无法推断出结果 object 类型。 This happens due to the typescript issue that has been discussed excessively at the following github issue: https://github.com/microsoft/TypeScript/issues/34933发生这种情况是由于 typescript 问题在以下 github 问题中被过度讨论: https://github.com/microsoft/TypeScript/issues/34933

Detail细节

In merge() code above A[number] type expands to union of array element types.在上面的merge()代码中, A[number]类型扩展为数组元素类型的并集。 UnionToIntersection metafunction converts the union to intersection. UnionToIntersection元函数将并集转换为交集。 Expand flattens the intersection so it becomes more readable by IntelliSense-like tools. Expand使交叉点变平,因此它变得更容易被类似 IntelliSense 的工具读取。

See the following references for more details on UnionToIntersection and Expand implementations:有关UnionToIntersectionExpand实现的更多详细信息,请参阅以下参考资料:
https://stackoverflow.com/a/50375286/5000057 https://stackoverflow.com/a/50375286/5000057
https://github.com/shian15810/type-expand https://github.com/shian15810/type-expand

Extra额外的

When using merge() function, it is likely that key duplicates in merged objects is an error.当使用merge() function 时,合并对象中的键重复很可能是一个错误。 The following function can be used to find such duplicates and throw Error :以下 function 可用于查找此类重复项并throw Error

export const mergeAssertUniqueKeys = <A extends object[]>(...a: [...A]) => {
  const entries = a.reduce((prev, obj) => {
    return prev.concat(Object.entries(obj));
  }, [] as [string, unknown][]);

  const duplicates = new Set<string>();
  entries.forEach((pair, index) => {
    if (entries.findIndex((p) => p[0] === pair[0]) !== index) {
      duplicates.add(pair[0]);
    }
  });

  if (duplicates.size > 0) {
    throw Error(
      [
        'objects being merged contain following key duplicates:',
        `${[...duplicates].join(', ')}`,
      ].join(' '),
    );
  }

  return Object.assign({}, ...a) as UnionToIntersection<A[number]>;
};

I like this answer from @Michael P. Scott .我喜欢@Michael P. Scott回答

I did it a little simpler since I was also looking for it.我做得更简单一些,因为我也在寻找它。 Let me share and explain it step by step.让我逐步分享和解释。

  1. Use the A type as the base for the merge type.使用A类型作为合并类型的基础。
  2. Get the keys of B that are not in A (a utility type like Exclude will help).获取不在A中的B的键(像Exclude这样的实用程序类型会有所帮助)。
  3. Finally, intersects types from steps #1 and #2 with & to get the combined type.最后,将步骤#1#2中的类型与& 相交以获得组合类型。
type Merge<A, B> = A & { [K in Exclude<keyof B, keyof A>]: B[K] };

Previous Answer is nice, but it is not actually accurate以前的答案很好,但实际上并不准确

This one is a bit better:这个好一点:

type Merge<A extends {}, B extends {}> = { 
  [K in keyof (A & B)]: (
    K extends keyof B 
      ? B[K] 
      : (K extends keyof A ? A[K] : never) 
  )
};

Difference can be checked in the playground可以在操场上检查差异

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM