typescript 打印脚本,合并对象类型?

kmynzznz  于 2022-12-27  发布在  TypeScript
关注(0)|答案(6)|浏览(139)

是否可以合并两个通用对象类型的 prop ?我有一个类似的函数:

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是一个字符串。实际返回值给出了正确的类型,但我不知道如何显式地写它。

plicqrtu

plicqrtu1#

更新TS4.1 +

最初的答案仍然有效(如果需要解释,应该阅读它),但是现在支持递归条件类型,我们可以将merge()写成variadic:

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代码链接

原始答案

由TypeScript标准库定义Object.assign()生成的交集类型是一个近似值,如果后面的参数具有与前面的参数同名的属性,则不能正确地表示所发生的情况。但是直到最近,这是在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类型 Package 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()只复制enumerable, own properties,并且类型系统不提供任何方式来表示要过滤的属性的可枚举性和所有权。这意味着merge({},new Date())在TypeScript中看起来像类型Date,即使在运行时,Date方法都不会被复制,并且输出基本上是{}
  • 此外,Spread的定义在 * missing * 属性和 * present with a undefined value * 属性之间并不是真正的distinguish。因此,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,你会发现交集已经被消除了,两个成分已经合并成一个类型,同样,在可赋值性方面,FooIdFoo之间没有真正的区别;只是后者在某些情况下更容易读懂。

4urapxun

4urapxun2#

我发现了一种语法,可以声明一个类型,该类型可以合并任意两个对象的所有属性。

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

此类型允许您指定任意两个对象A和B。
从这些对象中,创建一个Map类型,其键是从任一对象的可用键派生的。
然后,通过从源代码中查找相应的类型,将每个键Map到该键的类型。如果键来自B,则类型是来自B的键的类型。这是通过K extends keyof B ?完成的。这部分会问这样一个问题:“K是来自B的键吗?”要获得该键的类型,K,使用属性查找B[K]
如果密钥不是来自B,则它必须来自A,因此三元组完成:
x1米10英寸1x
所有这些都 Package 在对象符号{ }中,使其成为Map的对象类型,其键派生自两个对象,其类型Map到源类型。

oalqel3c

oalqel3c3#

如果要保留特性顺序,请使用以下解决方案。
在此观看实际应用。

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

u5rb5r594#

我认为您需要的是 union|)类型,而不是交集(&)类型。它更接近您想要的类型...

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我花了很多时间回到this page ...这是一本好书(如果你喜欢这类东西的话),提供了很多有用的信息

gdx19jrr

gdx19jrr5#

时间;日期

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 answer很好,为我工作了很多年,不幸的是,随着合并对象的计数增长到一定数量,typescript产生了下面的错误:

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

并且无法推导出结果的对象类型。这是由于在下面的github issue中已经讨论过太多的类型脚本问题: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()函数时,合并对象中的键重复很可能是错误的,可以使用下面的函数来查找这样的重复和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]>;
};
ruoxqz4g

ruoxqz4g6#

我喜欢这个来自@Michael P. Scott的answer
因为我也在寻找它,所以我做得简单一些。让我一步一步地分享和解释它。
1.使用A类型作为合并类型的基。
1.获取A中没有的B的键(Exclude这样的实用程序类型会有所帮助)。
1.最后,求步骤#1#2中的类型与&的交集,以获得组合类型。

type Merge<A, B> = A & { [K in Exclude<keyof B, keyof A>]: B[K] };

相关问题