Michal Kurz
Michal Kurz

Reputation: 2095

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

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?

Upvotes: 14

Views: 6794

Answers (5)

qn0361
qn0361

Reputation: 47

I did this:

type RequiredKeys<T> = {
    [K in keyof T as T[K] extends undefined ? never : K]: T[K];
};

type OptionalKeys<T> = {
    [K in keyof T as T[K] extends undefined ? K : never]: T[K];
};

type Union<T1 extends object, T2 extends object> = RequiredKeys<T1 | T2> &
    OptionalKeys<T1 | T2> & {
        [K in Exclude<keyof T1, keyof T2>]?: T1[K];
    } & {
        [K in Exclude<keyof T2, keyof T1>]?: T2[K];
    };

Upvotes: 0

riywo
riywo

Reputation: 1518

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

Upvotes: 12

wolfhoundjesse
wolfhoundjesse

Reputation: 1133

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  
}

Upvotes: 3

Lesiak
Lesiak

Reputation: 26064

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
}

Upvotes: 3

almostalx
almostalx

Reputation: 107

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

Upvotes: 1

Related Questions