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

4

13

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?

Retrochoir answered 15/1, 2021 at 20:26 Comment(0)
C
10

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

Caldeira answered 6/9, 2022 at 17:2 Comment(0)
W
3

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
}
Wisnicki answered 15/1, 2021 at 21:50 Comment(1)
That's a very good technical point. But honestly it is a risk I am willing to take, since as I said, type-guarding would be way too messy for my use-case. I'm willing to utilize type-assertion for this, but am having trouble deriving the asserted type - opened a new question here.Retrochoir
T
1

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

Tropophilous answered 15/1, 2021 at 21:16 Comment(1)
Yes, it does make sense, but is very inconvenient for me. Like I said in my post, type-guarding is not an option for me, for the reason mentioned.Retrochoir
C
1

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  
}
Cullum answered 6/10, 2021 at 15:49 Comment(1)
This is what I'm using ATM: #65751173Retrochoir

© 2022 - 2024 — McMap. All rights reserved.