繁体   English   中英

Typescript,合并object类型?

[英]Typescript, merge object types?

是否可以合并两个通用 object 类型的道具? 我有一个类似于此的 function:

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

我希望类型是 A 中不存在于 B 中的所有属性,以及 B 中的所有属性。

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

给出了一个相当奇怪的类型{a: number} & {b: string, a: string} ,虽然a是一个字符串。 实际返回给出了正确的类型,但我不知道我将如何明确地写它。

TS4.1+ 更新

原始答案仍然有效(如果需要解释,应该阅读它),但是现在支持递归条件类型,我们可以将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>;
}

你可以测试它:

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

Playground 链接到代码

原答案


Object.assign()TypeScript 标准库定义产生的交集类型是一个近似值,它不能正确表示如果后面的参数具有与前面的参数同名的属性会发生什么。 不过,直到最近,这是您在 TypeScript 的类型系统中所能做的最好的事情。

但是,从 TypeScript 2.8 中条件类型的引入开始,您可以使用更接近的近似值。 其中一项改进是使用此处定义的类型函数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>
  >;

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

让我们试试看:

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 好极了!


但请注意,这仍然是一个近似值:

  • Object.assign()只复制可枚举的、自己的属性,类型系统没有给你任何方式来表示要过滤的属性的可枚举性和所有权。 这意味着merge({},new Date())看起来像 TypeScript 的Date类型,即使在运行时不会复制任何Date方法并且输出本质上是{} 目前这是一个硬性限制。

  • 此外, Spread的定义并没有真正区分缺失的属性和存在未定义值的属性。 所以merge({ a: 42}, {a: undefined})被错误地输入为{a: number}而应该是{a: undefined} 这可能可以通过重新定义Spread来解决,但我不是 100% 确定。 对于大多数用户来说,它可能不是必需的。 (编辑:这可以通过重新定义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在运行时是一个字符串,但在编译时间。 哎呀。

所以请注意; Object.assign()输入为一个交集或一个Spread<>是一种“尽力而为”的事情,并且可能会导致您在极端情况下误入歧途。

Playground 链接到代码


*注意: Id<T>是一种身份类型,原则上不应对该类型做任何事情。 有人在某个时候编辑了这个答案以将其删除并仅替换为T 这样的更改并不是不正确,确切地说,但它违背了目的……即迭代键以消除交叉点。 相比:

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 }

如果您检查IdFoo您将看到交集已被消除并且两个组成部分已合并为一个类型。 同样, IdFoo分配性而言, FooIdFoo之间没有真正的区别。 只是后者在某些情况下更容易阅读。

我认为您正在寻找更多的联合( | ) 类型而不是交叉 ( & ) 类型。 它更接近你想要的......

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

学习 TS 我花了很多时间回到这个页面......这是一个很好的阅读(如果你喜欢那种东西)并且提供了很多有用的信息

如果要保留属性顺序,请使用以下解决方案。

这里查看它的实际效果。

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;博士

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]>;
};

另一个答案的动机

jcalz 的回答很好,并且为我工作了很多年。 不幸的是,随着合并对象的数量增长到一定数量,typescript 会产生以下错误:

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

并且无法推断出结果 object 类型。 发生这种情况是由于 typescript 问题在以下 github 问题中被过度讨论: https://github.com/microsoft/TypeScript/issues/34933

细节

在上面的merge()代码中, A[number]类型扩展为数组元素类型的并集。 UnionToIntersection元函数将并集转换为交集。 Expand使交叉点变平,因此它变得更容易被类似 IntelliSense 的工具读取。

有关UnionToIntersectionExpand实现的更多详细信息,请参阅以下参考资料:
https://stackoverflow.com/a/50375286/5000057
https://github.com/shian15810/type-expand

额外的

当使用merge() function 时,合并对象中的键重复很可能是一个错误。 以下 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]>;
};

我喜欢@Michael P. Scott回答

我做得更简单一些,因为我也在寻找它。 让我逐步分享和解释。

  1. 使用A类型作为合并类型的基础。
  2. 获取不在A中的B的键(像Exclude这样的实用程序类型会有所帮助)。
  3. 最后,将步骤#1#2中的类型与& 相交以获得组合类型。
type Merge<A, B> = A & { [K in Exclude<keyof B, keyof A>]: B[K] };

以前的答案很好,但实际上并不准确

这个好一点:

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

可以在操场上检查差异

暂无
暂无

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

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