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:
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
.
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 // ***
};
}
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 // ***
};
}