[英]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
是一个字符串。 实际返回给出了正确的类型,但我不知道我将如何明确地写它。
原始答案仍然有效(如果需要解释,应该阅读它),但是现在支持递归条件类型,我们可以将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;
} */
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<>
是一种“尽力而为”的事情,并且可能会导致您在极端情况下误入歧途。
*注意: 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
分配性而言, Foo
和IdFoo
之间没有真正的区别。 只是后者在某些情况下更容易阅读。
我认为您正在寻找更多的联合( |
) 类型而不是交叉 ( &
) 类型。 它更接近你想要的......
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] };
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 的工具读取。
有关UnionToIntersection
和Expand
实现的更多详细信息,请参阅以下参考资料:
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的回答。
我做得更简单一些,因为我也在寻找它。 让我逐步分享和解释。
type Merge<A, B> = A & { [K in Exclude<keyof B, keyof A>]: B[K] };
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.