Home > Mobile >  Pick properties from object while respecting union
Pick properties from object while respecting union

Time:09-14

I have created a union where an event is either "off-site" or "on-site". I then create a function which can receive an event, but i want to pick some properties. TS is highlighting enrollment.event as invalid because he doesn't know whether it respects the union. Is it possible to some how tell it, that it does. Example:

TS Playground: https://www.typescriptlang.org/play?#code/C4TwDgpgBAKgogNwgO2FAvFAFDA8sgZQEtgJEU0AfWXAM1uNPNQEooAyKAbwCgooiAEwBcUAM7AATkWQBzANw8Avjx6hINQiTJJUGbnwFiAMgHsAxgENgRU8gCCgwZIhixo2pYA2YiIv5e1qLIAK5eXv5QXnLBYRHKqurQePSMOhT6vPxEJhbWtg5OLm6iUiF hoHAsQC2AEYQkpHRsrUNTQlq4MlwyJKm4TUZmFlQIUKiEtJykRC61ZppzFQ0qdrLiio8tCHI5jZ24pZIvf2DFFgQovDzbKPmdhJQNSCnA15DqNdv53ojhvxxiIoAByFBnD4UAC0AEYQQAaAFQOYUUSjfjZXJWA6FZyudzIgB0OTM2IKjjxbkRGIxVVEEEJVWpNKiMSJLSRKn4KiUQA

My code:

type TEvent = (TOnSiteEvent | TOffSiteEvent) & {
  id: string;
}

type TOnSiteEvent = {
  isLocationAddress: false;
  lat: null;
  lng: null;
}

type TOffSiteEvent = {
  isLocationAddress: true;
  lat: number;
  lng: number;
}

type TEnrollment = {
  uid: string;
  event: TOnSiteEvent | TOffSiteEvent;
}

function saveEnrollment(e: TEvent) {
  const myEnrollment: TEnrollment = {
    uid: 'enrollment-1',
    event: {
      isLocationAddress: e.isLocationAddress,
      lat: e.lat,
      lng: e.lng
    }
  }
}

If I set event just to event: e then it works, as it sees it respects the union, but i just want to pick certain properties out as seen above.

I have even tried adding gaurd statements into saveEnrollment method above but its not fixing the problem, these were the gaurds I used:

if (e.isLocationAddress === true && (e.lat === null || e.lng === null)) {
  throw new Error('lat and lng must be provided for on-site events');
}
if (e.isLocationAddress === false && (e.lat !== null || e.lng !== null)) {
  throw new Error('lat and lng must not be provided for off-site events');
}

CodePudding user response:

When you do it individually, TypeScript doesn't know that the type of isLocationAddress (true or false) matches up with lat/lng (which have to be number in the true case, but null in the false case). You can do it by ensuring that you grab the event information from e as a unit, like this:

function saveEnrollment(e: TEvent) {
    const { id, ...event } = e;  // ***
    const myEnrollment: TEnrollment = {
        uid: 'enrollment-1',
        event                     // ***
    };
}

That way, TypeScript knows that isLocationAddress's type matches with the type of lat and lng.

Updated playground


In a comment you asked:

This is very interesting thank you! Is there a way to do the opposite to omit? Because in my actual code event has a ton of things, omitting each one would be very difficult and more things may be added.

I can't think of a way of doing it that doesn't involve a type assertion. :-( But it's a really safe type assertion tucked away in a small reusable function:

type ExtractSitekeys = "isLocationAddress" | "lat" | "lng";
function extractSiteEvent<T extends TOnSiteEvent | TOffSiteEvent>(e: T): Pick<TOnSiteEvent, ExtractSitekeys> | Pick<TOffSiteEvent, ExtractSitekeys> {
    const { isLocationAddress, lat, lng } = e;
    return { isLocationAddress, lat, lng } as TOnSiteEvent | TOffSiteEvent;
}

You know that's a valid assertion, even if TypeScript doesn't. Then usage is:

function saveEnrollment(e: TEvent) {
    const event = extractSiteEvent(e);  // ***
    const myEnrollment: TEnrollment = {
        uid: 'enrollment-1',
        event                           // ***
    };
}

Playground link

The Pick part is important. Without that, we wouldn't get any error from TypeScript if you added a property to TOnSiteEvent or TOffSiteEvent; the type assertion would be wrong (example). But with the Pick part, we correctly get an error in that case (example).


That said, you'd avoid this problem entirely if you made the site event a nested object rather than just an intersection with everything else, like it is in TEnrollment:

type TEvent = {
    id: string;
    event: TOnSiteEvent | TOffSiteEvent;    // ***
};
// ...
function saveEnrollment(e: TEvent) {
    const { event } = e;                // ***
    const myEnrollment: TEnrollment = {
        uid: 'enrollment-1',
        event                           // ***
    };
}

Playground link

  • Related