打字稿,合并对象类型?

Posted

技术标签:

【中文标题】打字稿,合并对象类型?【英文标题】:Typescript, merge object types? 【发布时间】:2018-09-15 21:05:10 【问题描述】:

是否可以合并两个通用对象类型的 props? 我有一个类似这样的功能:

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 &amp; b: string, a: string 类型,a 虽然是一个字符串。 实际的返回给出了正确的类型,但我不知道如何显式编写它。

【问题讨论】:

“明确”是指A &amp; B?是你要找的吗? 【参考方案1】:

我认为您正在寻找更多的 union (|) 类型,而不是交集 (&amp;) 类型。它更接近你想要的......

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...这是一本很好的读物(如果您喜欢这类东西)并提供了很多有用的信息

【讨论】:

这不太可能是真的。您可以从 OP 尝试merge(a: 42, b: "foo", a: "bar").b 在您的第一个示例中,合并的结果将是 a: 1。使用交叉点将其正确输入为 a: number【参考方案2】:

TS4.1+ 更新

原来的答案仍然有效(如果需要解释,您应该阅读它),但现在支持 recursive conditional types,我们可以将 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 link to code

原始答案


TypeScript standard library definition of Object.assign() 生成的交集类型是 approximation,它不能正确表示如果后面的参数具有与前面的参数同名的属性会发生什么。不过,直到最近,这是您在 TypeScript 的类型系统中所能做的最好的事情。

从在 TypeScript 2.8 中引入 conditional types 开始,但是,您可以使用更接近的近似值。其中一项改进是使用类型函数Spread&lt;L,R&gt; 定义here,如下所示:

// 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 &amp; number。耶!


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

Object.assign() 仅复制enumerable, own properties,类型系统无法为您提供任何方式来表示要过滤的属性的可枚举性和所有权。这意味着merge(,new Date()) 看起来像 TypeScript 的类型Date,即使在运行时不会复制任何Date 方法并且输出本质上是。这是目前的硬性限制。

此外,Spread 的定义在缺少 属性和存在未定义值 的属性之间并不是真正的distinguish。所以merge( a: 42, a: undefined) 被错误地输入为a: number 而它应该是a: undefined。这可能可以通过重新定义Spread 来解决,但我不是 100% 确定。对于大多数用户来说,这可能不是必需的。 (编辑:这可以通过重新定义type OptionalPropertyNames&lt;T&gt; = [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&lt;&gt; 是一种“尽力而为”的事情,在极端情况下可能会导致您误入歧途。

Playground link to code


*注意:Id&lt;T&gt; 是一种身份类型,原则上不应对该类型做任何事情。有人在某个时候编辑了此答案以将其删除并仅替换为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 在可分配性方面没有真正的区别。只是后者在某些情况下更容易阅读。

【讨论】:

这看起来不错!在我的例子中,assign 只会用于对象文字,所以大多数边缘情况都很好!我确实对您对OptionalPropertyNames 的修复有疑问。 Spread 类型抱怨,哪个。我很想修好! Prettier 删除了关键的 - :) 有没有将 3+ 个对象合并为一个对象类型的解决方案? 当然,您可以使用Spread&lt;Spread&lt;L,M&gt;,R&gt; 或类似名称。我假设您想采用元组类型[A,B,C,D,E,F] 并自动将其转换为Spread&lt;Spread&lt;Spread&lt;Spread&lt;Spread&lt;A,B&gt;,C&gt;,D&gt;,E&gt;,F&gt;... 但由于语言中的递归限制,我认为您无法获得任意长度的元组。您可以硬编码类型达到一定的长度,例如type SpreadTuple&lt;T extends any[], L extends number = T['length']&gt; = L extends 0 ? never : L extends 1 ? T[0] : L extends 2 ? Spread&lt;T[0],T[1]&gt; : ... 有人将它作为 NPM 包发布吗?【参考方案3】:

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

看到它在行动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] 

【讨论】:

【参考方案4】:

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

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

【讨论】:

以上是关于打字稿,合并对象类型?的主要内容,如果未能解决你的问题,请参考以下文章

打字稿中的通用对象类型

为啥打字稿抱怨对象必须是扩展类型中的对象

打字稿:传播类型只能从对象类型创建

打字稿中具有联合类型键的松散类型对象

打字稿:在对象内推断通用对象的类型

对象打字稿中的条件类型