打字稿中对象文字的类型安全合并

Posted

技术标签:

【中文标题】打字稿中对象文字的类型安全合并【英文标题】:Type safe merge of object literals in typescript 【发布时间】:2020-04-28 06:08:33 【问题描述】:

我想合并两个打字稿对象(使用对象扩展):

var one =  a: 1 
var two =  a: 2, b: 3 
var m = ...one, ...two // problem as property `a` is overwritten

我想使用类型系统来确保第二个对象中的任何属性都不会覆盖第一个对象中的任何属性。我不确定为什么以下解决方案不起作用:

type UniqueObject<T extends [K in keyof U]?: any, U> =
    [K in keyof U]: T[K] extends U[K] ? never : U[K]

var one =  a: 1 
var two1 =  a: 2, b: 3 
var two1_: UniqueObject<typeof one, typeof two1> = two1 // errors correctly
var two2 =  a: undefined, b: 1 
var two2_: UniqueObject<typeof one, typeof two2> = two2 // passes incorrectly

我当时认为工作的Another version from a year ago 用undefined extends U[K] 代替了T[K] extends U[K]

type UniqueObject<T extends [K in keyof U]?: any, U> =
    [K in keyof U]: undefined extends T[K] ? U[K]: never

这两个都不起作用。我怀疑这是因为undefined extends U[K]T[K] extends U[K] 都是错误的,因为T 中的属性K 是可选的。不确定如何或是否有可能解决这个问题。

【问题讨论】:

【参考方案1】:

您的两个版本或多或少是等效的 - 只有条件类型中的真/假分支被切换。

约束T extends [K in keyof U]?: any有点问题:当你去掉two中的a时,会触发错误Type ' a: number; ' has no properties in common with type ' b?: any; ,其实应该是成功案例。

还要注意,生成的类型 merge 不包含两种类型的合并类型定义。我们可以修改声明:

type UniqueObject<T, U> =
    T &  [K in keyof U]: K extends keyof T ? never : U[K] 

现在,编译器正确地出现了重复的 a 属性错误:

var one =  a: 1 
var two =  a: 2, b: 3 
//                                                        v a becomes never here
type Merge = UniqueObject<typeof one, typeof two> //  a: never; b: number; 
const res: Merge =  ...one, ...two  // errors now, cannot assign number to never

在下文中,我稍微简化了类型并将所有内容打包在一个紧凑的辅助函数中以控制类型 + 扩展运算符:

function mergeUnique<T extends object, U extends object &  [K in keyof U]: K extends keyof T ? never : U[K] >(o1: T, o2: U) 
    return  ...o1, ...o2 


const res21 = mergeUnique( a: 1 ,  b: 3 )
const res22 = mergeUnique( a: 1 ,  a: 2, b: 3 ) // error
const res23 = mergeUnique( a: 1, c: 5 ,  b: 3 )
const res24 = mergeUnique( a: 1,  a: undefined ) // error

Code sample

【讨论】:

太棒了。谢谢你。是的,更改为K extends keyof T 是关键。我又添加了 3 个测试用例,并提出了建议的更改。随意再次编辑。 实际上,我添加了一些“错误”类型的场景。我目前无法找到解决这些问题的方法,因为它归结为keyof ([index: string]: number) => string 匹配所有K in keyof U 并使它们成为never 嗯,您在这里提出了一些新案例(丢失了编辑答案的轨道)。最初的问题是关于合并两个对象文字。具有显式索引签名类型的对象也引入了另一个维度。我建议,如果答案不能完全满足您的需求,请扩展您的问题,或者改写您自己的答案。干杯! 同意并道歉,大脑 v 累了。我已经编辑了问题标题以使其仅与对象文字有关,我已经进行了小修改(如果您不同意,请再次编辑),并且我在这里提出了一个新问题:***.com/q/59689060/539490【参考方案2】:

试试

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

【讨论】:

请在您的答案中添加一些解释,以便其他人可以从中学习【参考方案3】:

采用@ford04's answer 并将其扩展为多个可选值:

function safe_merge<
    O1,
    O2 extends  [K2 in keyof O2]: K2 extends keyof O1 ? never : O2[K2] ,
    O3 extends  [K3 in keyof O3]: K3 extends keyof O1 ? never : (K3 extends keyof O2 ? never : O3[K3]) ,
    O4 extends  [K4 in keyof O4]: K4 extends keyof O1 ? never : (K4 extends keyof O2 ? never : (K4 extends keyof O3 ? never : O4[K4])) ,
    O5 extends  [K5 in keyof O5]: K5 extends keyof O1 ? never : (K5 extends keyof O2 ? never : (K5 extends keyof O3 ? never : ( K5 extends keyof O4 ? never : O5[K5]))) ,
    O6 extends  [K6 in keyof O6]: K6 extends keyof O1 ? never : (K6 extends keyof O2 ? never : (K6 extends keyof O3 ? never : ( K6 extends keyof O4 ? never : (K6 extends keyof O5 ? never : O6[K6])))) ,
    O7 extends  [K7 in keyof O7]: K7 extends keyof O1 ? never : (K7 extends keyof O2 ? never : (K7 extends keyof O3 ? never : ( K7 extends keyof O4 ? never : (K7 extends keyof O5 ? never : (K7 extends keyof O6 ? never : O7[K7]))))) ,
    O8 extends  [K8 in keyof O8]: K8 extends keyof O1 ? never : (K8 extends keyof O2 ? never : (K8 extends keyof O3 ? never : ( K8 extends keyof O4 ? never : (K8 extends keyof O5 ? never : (K8 extends keyof O6 ? never : (K8 extends keyof O7 ? never : O8[K8])))))) ,
    O9 extends  [K9 in keyof O9]: K9 extends keyof O1 ? never : (K9 extends keyof O2 ? never : (K9 extends keyof O3 ? never : ( K9 extends keyof O4 ? never : (K9 extends keyof O5 ? never : (K9 extends keyof O6 ? never : (K9 extends keyof O7 ? never : (K9 extends keyof O8 ? never : O9[K9]))))))) ,
>(
    o1: O1,
    o2: O2 = ( as any),
    o3: O3 = ( as any),
    o4: O4 = ( as any),
    o5: O5 = ( as any),
    o6: O6 = ( as any),
    o7: O7 = ( as any),
    o8: O8 = ( as any),
    o9: O9 = ( as any),
): O1 & O2 & O3 & O4 & O5 & O6 & O7 & O8 & O9 
    return  ...o1, ...o2, ...o3, ...o4, ...o5, ...o6, ...o7, ...o8, ...o9 



const obj_1 = 1:1
const obj_2 = 2:1
const obj_3 = 3:1
const obj_4 = 4:1
const obj_5 = 5:1
const obj_6 = 6:1
const obj_7 = 7:1
const obj_8 = 8:1
const obj_9 = 9:1


// should not error
safe_merge(obj_1)
safe_merge(obj_1, obj_2)
safe_merge(obj_1, obj_2, obj_3)
safe_merge(obj_1, obj_2, obj_3, obj_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9)


// declare objects with keys conflicting with existing objects
const obj_2_1 = 2:1, 1: 1
const obj_2_1b = 2:1, 1: undefined

const obj_3_1 = 3:1, 1:1
const obj_3_2 = 3:1, 2:1

const obj_4_1 = 4:1, 1:1
const obj_4_2 = 4:1, 2:1
const obj_4_3 = 4:1, 3:1

const obj_5_1 = 5:1, 1:1
const obj_5_2 = 5:1, 2:1
const obj_5_3 = 5:1, 3:1
const obj_5_4 = 5:1, 4:1

const obj_6_1 = 6:1, 1:1
const obj_6_2 = 6:1, 2:1
const obj_6_3 = 6:1, 3:1
const obj_6_4 = 6:1, 4:1
const obj_6_5 = 6:1, 5:1

const obj_7_1 = 7:1, 1:1
const obj_7_2 = 7:1, 2:1
const obj_7_3 = 7:1, 3:1
const obj_7_4 = 7:1, 4:1
const obj_7_5 = 7:1, 5:1
const obj_7_6 = 7:1, 6:1

const obj_8_1 = 8:1, 1:1
const obj_8_2 = 8:1, 2:1
const obj_8_3 = 8:1, 3:1
const obj_8_4 = 8:1, 4:1
const obj_8_5 = 8:1, 5:1
const obj_8_6 = 8:1, 6:1
const obj_8_7 = 8:1, 7:1

const obj_9_1 = 9:1, 1:1
const obj_9_2 = 9:1, 2:1
const obj_9_3 = 9:1, 3:1
const obj_9_4 = 9:1, 4:1
const obj_9_5 = 9:1, 5:1
const obj_9_6 = 9:1, 6:1
const obj_9_7 = 9:1, 7:1
const obj_9_8 = 9:1, 8:1


// should error
safe_merge(obj_1, obj_2_1)
safe_merge(obj_1, obj_2_1b)

safe_merge(obj_1, obj_2, obj_3_1)
safe_merge(obj_1, obj_2, obj_3_2)

safe_merge(obj_1, obj_2, obj_3, obj_4_1)
safe_merge(obj_1, obj_2, obj_3, obj_4_2)
safe_merge(obj_1, obj_2, obj_3, obj_4_3)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_4)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_5)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_6)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_6)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_7)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_6)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_7)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_8)

【讨论】:

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

在打字稿中获取字典/对象键作为元组

打字稿中的通用对象类型

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

对象打字稿中的条件类型

我们如何在打字稿中获得嵌套对象类型

转换对象属性,以便在打字稿中保留隐式类型