Tom
Tom

Reputation: 8137

How to merge two partials to a complete object?

I have an interface that requires all props. However, creating the object is a two-step process. Therefore I am planning to create 2 Partial versions of the object, and then merge them together to satisfy the non-partial interface. Example:

interface IComplete {
    a: string,
    b: string
}

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

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

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

Even though together is always complete, the compiler complains:

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'.

Playground

Is there a recommended way to achieve this? It is important for my case that the interface IComplete is not made partial, i.e. the props a and b are never null or undefined.

Upvotes: 16

Views: 12317

Answers (3)

Hyuck Kang
Hyuck Kang

Reputation: 1789

Frankly, I do not fully understand why typescript transpiler does not permit that syntax.

But the below codes can bypass it and achieve what you want.

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

Upvotes: 3

jcalz
jcalz

Reputation: 329953

@RyanCavanaugh's answer is good. One way to let type inference do its work while still getting the benefits of the compiler ensuring that part1 and part2 are Partial<IComplete> is to use a helper function as below:

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
}

In the above, both part1 and part2 are constrained by asPartialIComplete to be valid Partial<IComplete> objects (so the parameters of the c and d methods are inferred as string and number respectively). But the types of part1 and part2 are narrow enough for the compiler to realize that typeof part1 & typeof part2 is IComplete.

Hope that also helps. Good luck!

Upvotes: 9

Ryan Cavanaugh
Ryan Cavanaugh

Reputation: 221302

The easiest way would be to remove the type annotations and let type inference do its work:

interface IComplete {
    a: string,
    b: string
}

const part1 = {
    a: 'hello'
}

const part2 = {
    b: 'world'
}

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

This does have certain deleterious effects, e.g. parameters in function expressions in part1 or part2 won't get inferred types.

Instead you can use Pick - unfortunately you do have to write out the keys that you're picking from each type when using this approach:

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
}

Upvotes: 7

Related Questions