Home > Net >  Typescript throws error TS2345 and requires the properties of union type interface to be declared
Typescript throws error TS2345 and requires the properties of union type interface to be declared

Time:10-24

Assume the Contact Interface and the ContactDeletedEvent and ContactStatusChangedEvent Interfaces that represent events of the Contact type, each event defined with its own set of properties.

Having grouped the events under a single Interface called ContactEvents, I wrote a function to handle the different event types based on the property name of ContactEvents that describes the event type.

Instead of detecting which properties are appropriate for the "deleted" event, TypeScript throws an error that all the properties for a union type of ContactDeletedEvent & ContactStatusChangedEvent are required.

Here is the .ts code

type ContactStatus = "active" | "inactive" | "new";

interface Contact {
  id: number;
  name: string;
  status: ContactStatus;
  address: string;
}


interface ContactEvent {
  contactId: Contact["id"];
}

interface ContactDeletedEvent extends ContactEvent {}

interface ContactStatusChangedEvent extends ContactEvent {
  oldStatus: Contact["status"];
  newStatus: Contact["status"];
}

interface ContactEvents {
  deleted: ContactDeletedEvent;
  statusChanged: ContactStatusChangedEvent;
  // ... and so on
}

function handleEvent<T extends keyof ContactEvents>(
  eventName: T,
  handler: (evt: ContactEvents[T]) => void
) {
  if (eventName === "deleted") {
    handler({ contactId: 1 });
  } else {
    handler({ contactId: 1, oldStatus: "active", newStatus: "inactive" });
  }
}

handleEvent("statusChanged", (evt) => evt);

The output of the tsc:

src/demo.ts:44:13 - error TS2345: Argument of type '{ contactId: number; }' is not assignable to parameter of type 'ContactEvents[T]'.
  Type '{ contactId: number; }' is not assignable to type 'ContactDeletedEvent & ContactStatusChangedEvent'.
    Type '{ contactId: number; }' is missing the following properties from type 'ContactStatusChangedEvent': oldStatus, newStatus

44     handler({ contactId: 1 });
               ~~~~~~~~~~~~~~~~

CodePudding user response:

TypeScript does not do any narrowing on generic types. Even after checking if eventName is "deleted", both eventName and handler are still unions which leads to the error when trying to call handler.

As a work-around, I would propose to use a function overload where the implementation function is not generic. The implementation will use a union of all valid eventName and handler combinations as the parameter.

We can generate those combinations like this:

type HandleEventParams = {
  [K in keyof ContactEvents]: [
    eventName: K, 
    handler: (evt: ContactEvents[K]) => void
  ]
}[keyof ContactEvents]

Now we need to overload the function.

function handleEvent<T extends keyof ContactEvents>(
  eventName: T,
  handler: (evt: ContactEvents[T]) => void
): void
function handleEvent(
  ...args: HandleEventParams
) {
  const [eventName, handler] = args

  if (eventName === "deleted") {
    handler({ contactId: 1 });
  } else {
    handler({ contactId: 1, oldStatus: "active", newStatus: "inactive" });
  }
}

This removes the errors and gives us a type-safe implementation.

The function will still be called as before.

handleEvent("statusChanged", (evt) => evt);

Playground

  • Related