Destructure/access a property that may or may not exist on an object union type

Viewed 2324

I get the following errors:

type Union = { type: "1"; foo: string } | { type: "2"; bar: number };

function doSomething = (object: Union) => {
  const { foo } = object
  //      ^ TS2339: Property 'foo' does not exist on type 'Union'.
  console.log(object.bar)
  //                 ^ TS2339: Property 'bar' does not exist on type 'Union'.
}

Desired outcome:

typeof foo === string | undefined
typeof bar === number | undefined

How can I access the properties without explicitly type-guarding, for example:

const foo = o.type === 1 ? o.foo : undefined
const bar = o.type === 2 ? o.bar : undefined

this is not really an option for me, beacuse I'm working with large unions, where target properties may or may not be present on many objects, it would be a complete mess.

What other options do I have?

4 Answers

Check comment in Accessing property in union of object types fails for properties not defined on all union members #12815

The issue here is that because B doesn't declare an a property, it might at run-time have an a property of any possible type (because you can assign an object with any set of properties to a B as long as it has a b property of type string). You can make it work explicitly declaring an a: undefined property in B (thus ensuring that B won't have some random a property):

type A = { a: string; } 
type B = { b: string; a: undefined }
type AorB = A | B;

declare const AorB: AorB;

if (AorB.a) {
   // Ok
}

This behaviour kinda makes sense because TS doesn't know which object from the Union it's dealing with and the property doesn't exist in some cases.

I'm not sure if that's what you are looking for, but you could try something like

type Union = { type: "1"; foo: string } | { type: "2"; bar: number };

function doSomething = (object: Union) => {
  if ('foo' in object) {
    const { foo } = object
  }

  if ('bar' in object) {
    console.log(object.bar)
  }
}

Here's a playground

The most convenient way I've found is to cast the variables based on their type.

type Type1 = { type: "1"; foo: string }
type Type2 = { type: "2"; bar: number }    
type Union = Type1 | Type2

function doSomething = (object: Union) => {
    const { foo } = object as Type1
    const { bar } = object as Type2
    const { type } = object  
}

You can set never for unused properties, then TS can understand the type of these properties as optional.

type Type1 = { type: "1"; foo: string, bar?: never }
type Type2 = { type: "2"; foo?: never, bar: number }    
type Union = Type1 | Type2

const doSomething = (object: Union) => {
    const { type, foo, bar } = object
    console.log(type, foo, bar)
}

doSomething({ type: "1", foo: "foo" }) // [LOG]: "1",  "foo",  undefined 
doSomething({ type: "2", bar: 2 }) // [LOG]: "2",  undefined,  2 

TS Playground link: https://www.typescriptlang.org/play?#code/C4TwDgpgBAKuEEYoF4oG8qkgLigIgTwG4oAzAe3NwGdgAnASwDsBzAGigCMBDOgflxMIANwh0oAXwCwAKCzQ4kAEwp0meLjxLiZSgKhDRdDjzqCArgFtOYyVHtRZ8qAFUmDck1WLEUAD6w8EqysgDGnrRQACbkAMrklhDAABbMLKoAFOScAFYQocC4bh5MAJQoAHzosg5Q4UyRGPIcFOQmvHao2XkFNQ711OQANhAAdEPkLBnNum1cvKWy0jKyMfGJKWkZTRr4hC2Umq14kosyawlJqazb6jj42u1mUCoSZ0A

Related