typescriptmethod-signature

Can a function type accept intersected types as arguments in TypeScript?


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?


Solution

  • 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.

    Playground link to code