Say I've got these types defined:
export type Event = {
name: EventName,
timestamp: Timestamp,
};
export type EventWithMetadata = Event & {
deviceId: ID,
};
I'm looking to define a common function interface like so:
export type SideEffect = (event: Event, run?: Run) => Partial<Run>;
and using it with this function
// Note: many of these side-effect functions exist, with multiple specializations of the base Event
// type as potential arguments. This is just one example.
const updateTiming: SideEffect = (event: EventWithMetadata, run) => {
log.debug('updateTiming: %j', event);
return {
lastUpdated: event.timestamp,
deviceId: event.deviceId,
};
};
but then I hit this error:
error TS2322: Type '(event: EventWithMetadata, run: Run | undefined) => { startTime: number; deviceId: string; }' is not assignable to type 'SideEffect'.
Types of parameters 'event' and 'event' are incompatible.
Type 'Event' is not assignable to type 'EventWithMetadata'.
Property 'deviceId' is missing in type 'Event' but required in type '{ deviceId: string; }'.
which makes sense: Typescript thinks I'm trying to downcast Event
into EventWithMetadata
, which obviously can't happen.
After a lot of searching on generics and overloads and union types, the best answer I could find is this one. I really hate the result and wouldn't want to make it a pattern:
const updateTiming: SideEffect = (inputEvent: Event, run) => {
const event = inputEvent as EventWithMetadata; // <- This sucks.
log.debug('updateTiming: %j', event);
return {
lastUpdated: event.timestamp,
deviceId: event.deviceId,
};
};
Are there better solutions than casting?
If you need updateTiming
to handle EventWithMetadata
specifically, then the type of updateTiming
needs to know about EventWithMetadata
. In order for SideEffect
to know about EventWithMetadata
and still work with other Event
types, you probably want it to be generic in the particular subtype of Event
that it handles:
type SideEffect<T extends Event> =
(event: T, run?: Run) => Partial<Run>;
Then updateTiming
is a SideEffect<EventWithMetadata>
and not just a SideEffect
:
const updateTiming: SideEffect<EventWithMetadata> = (event, run) => {
console.debug('updateTiming: %j', event);
return {
lastUpdated: event.timestamp,
deviceId: event.deviceId,
};
}; // okay
Note that it's not possible to hold onto a SideEffect
without knowing what it handles, but that's a good thing, because otherwise you could actually pass it the wrong Event
. If you think you need a SideEffect[]
without a type argument, then you will have to refactor so that you can distinguish events and event handlers somehow via a test (ideally both handlers and events would be a discriminated union but otherwise you could write custom type guard functions). But that's out of scope for this question as asked.