如何将两个部分合并为一个完整的对象?

12
我有一个需要所有属性的接口。然而,创建对象是一个两步骤的过程。因此,我计划创建2个对象的“Partial”版本,然后将它们合并在一起以满足非部分接口。示例:
interface IComplete {
    a: string,
    b: string
}

const part1: Partial<IComplete> = {
    a: 'hello'
}

const part2: Partial<IComplete> = {
    b: 'world'
}

const together: IComplete = {
    ...part1,
    ...part2
}

尽管“together”总是完整的,但编译器仍然报错:
Type '{ a?: string; b?: string; }' is not assignable to type 'IComplete'.
   Property 'a' is optional in type '{ a?: string; b?: string; }' but required in type 'IComplete'.

游乐场

有没有推荐的方法来实现这个?对于我的情况来说,很重要的一点是接口IComplete不能被部分化,也就是说属性a和b永远不会为null或undefined。
3个回答

8

@RyanCavanaugh的回答很好。一种让类型推断发挥作用,同时仍然获得编译器确保part1part2Partial<IComplete>的好处是使用下面的辅助函数:

interface IComplete {
  a: string,
  b: string,
  c: (x: string) => number,
  d: (x: number) => string
}

// helper function
const asPartialIComplete = <T extends Partial<IComplete>>(t: T) => t;

const part1 = asPartialIComplete({
  a: 'hello',
  c: (x) => x.length
})

const part2 = asPartialIComplete({
  b: 'world',
  d: (x) => x + ""
});

const together: IComplete = {
  ...part1,
  ...part2
}

在上述代码中,part1part2都受到asPartialIComplete的限制,使得cd方法的参数类型分别被推断为stringnumber,并且它们都是有效的Partial<IComplete>对象。但是,part1part2的类型足够窄,编译器可以意识到typeof part1 & typeof part2IComplete
希望这也能帮到你,祝你好运!

我不认为使用这个函数比只写const part1: Partial<IComplete> = ...有什么好处。在TS 4.8中,这对我很有效。 - Arthur_J

5
最简单的方法就是去掉类型注释,让类型推断完成其工作:
interface IComplete {
    a: string,
    b: string
}

const part1 = {
    a: 'hello'
}

const part2 = {
    b: 'world'
}

const together: IComplete = {
    ...part1,
    ...part2
}

这确实会产生一定的负面影响,例如在part1part2中的函数表达式中的参数无法推断类型。

相反,您可以使用Pick - 不幸的是,在使用此方法时,您必须写出从每个类型选取的键:

interface IComplete {
    a: string,
    b: string,
    c: number
}

const part1: Pick<IComplete, "a"> = {
    a: 'hello'
}

const part2: Pick<IComplete, "b" | "c"> = {
    b: 'world',
    c: 43
}

const together: IComplete = {
    ...part1,
    ...part2
}

为什么使用展开运算符不起作用?例如,在此处,我使用createState函数的结果为T和Partial<T>。https://www.typescriptlang.org/play?ts=3.9.2#code/JYOwLgpgTgZghgYwgAgMpjpZBvAUM5DAcwC5kBnMKUI-ZGAewbMupFoF9dc4AjVxGGQIANnHLlkAFQiUAPFIB8OOgAcArrxHAEyPgIRDKmCGSkBuOsKgQT6EwApVUBqrIAFOFDDA4IhYoAlCoEoTZg6lAgOAB0cWAAFsDkMcaQADTIcTHOrhyWBFwcQA - Lonli-Lokli

3
坦率地说,我不完全明白为什么TypeScript转译器不允许那种语法。但是下面的代码可以绕过它并实现您想要的功能。
interface IComplete {
    a: string,
    b: string
}

const part1: Partial<IComplete> = {
    a: 'hello'
}

const part2: Partial<IComplete> = {
    b: 'world'
}

const together: IComplete = {
    ...part1,
    ...part2
} as IComplete

1
好奇这种解决方案是否有任何缺点。它似乎是最易读的。 - Tom
7
没有静态验证能够证明part1part2共同贡献了IComplete所有的键。 - Ryan Cavanaugh
2
缺点是 together 可能不是一个 IComplete,如果你将 part2 改为 {a: 'whoops'},就会发现这一点。 - jcalz
谢谢,很有道理!我很难选择一个被接受的答案...如果你看到一个明显的赢家,请告诉我。 - Tom
1
是的,那些是公正的批评。我的回答并不保证正确的类型,这可能会导致未被注意到的错误。我认为@jcalz的回答是一个典范的回答,符合typescript的使命。 - Hyuck Kang
嗯,我认为这也是Typescript应该做的事情。在你的例子中,当没有提供接口的所有属性时,它应该显示一个警告,而在所提到的情况下它没有这样做是相当失败的。我想知道这是否是一个已知的错误。 - Gerry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接