Home > other >  TypeScript loses type knowledge inside of callback function (Ex. .filter and .some callback function
TypeScript loses type knowledge inside of callback function (Ex. .filter and .some callback function

Time:09-03

I'm seeing some very strange TypeScript behavior that I don't understand. Here is the scenario:

I have three different interfaces that describe an event. They look like this

interface ReceiveMessageEvent {
  event: 'receive-message';
  payload: {
    action: 'receive-message';
    businessSMSId: number;
    message: {
      businessSMSId: number;
      from: string;
      to: string;
    };
  };
}

interface ReadMessageEvent {
  event: 'read-message';
  payload: {
    action: 'read-message';
    businessSMSId: number;
    customerNumber: string;
    inboxNumber: string;
  };
}

interface SetThreadsEvent {
  event: 'set-threads';
  threads: any[];
}

As you can see, all three interfaces have an event property that is a specific string, meaning there can only be three possible options, either 'receive-message', 'read-message', or 'set-threads'. This is important because next I have a switch statement where the case is this event property:

switch (action.payload.event) {
  case 'receive-message':
    // ...
  case 'read-message':
    // ...
  case 'set-threads':
    // ...
}

So, since the case is the event property, I'd expect TypeScript to know the shape of the rest of the data depending on which case we're in. Now, is DOES do this correctly (as I'd expect), except in one specific scenario, which is where my confusion is coming from. And that case is when I'm inside the callback function of the .filter method.

So for example:

switch (action.payload.event) {
  case 'receive-message':
    // This is correct and has no errors
    const test = action.payload.payload.message.to

    // This is giving me a TS error, even though I'm accessing the exact same property
    // As in the line above
    myArray.filter((thread) => thread.property === action.payload.payload.message.to)
                                                                 ^^^^^^^^ TS error here
  case 'read-message':
    // ...
  case 'set-threads':
    // ...
}

The exact error from the example above is this:

Property 'payload' does not exist on type 'ReceiveMessageEvent | ReadMessageEvent | SetThreadsEvent'.
  Property 'payload' does not exist on type 'SetThreadsEvent'.

It's as if Typescript loses its type knowledge once I enter the callback function. The code does actually work exactly as expected, it's just TypeScript telling me there's an error even though there doesn't seem to be.

Lastly I'll note that I can do some casting like this, and then the errors disappear, although I'd prefer not to do this unless I absolutely have to):

switch (action.payload.event) {
  case 'receive-message':
    myArray.filter((thread) => thread.property === (action.payload as ReceiveMessageEvent).payload.message.to)
  case 'read-message':
    // ...
  case 'set-threads':
    // ...
}

Is there a reason for this behavior, or this potentially a bug within TypeScript?

CodePudding user response:

It's as if Typescript loses its type knowledge once I enter the callback function

Yep, that's it. And that behavior is deliberate. Or at least, it's the best typescript can do.

The issue is that typescript has no idea when the callback function will be called. You and i know that for .filter the callback will be called synchronously with nothing in between, but that's not the case for all callbacks. For example, the callback in setTimeout will be called asynchronously.

The type information doesn't identify whether it's synchronous or not, so typescript has to assume the worst case: asynchronous. If a callback is called asynchronously, then any arbitrary code may have run before the callback, and so the check you did to narrow down the type may no longer be accurate. Some code may have mutated the properties in the meantime.

The simplest fix for this is usually to assign the value you care about to a const while you're still in non-callback code, and then refer to the const in the callback code. With a const, typescript can then assume that it hasn't been reassigned.

case 'receive-message':
    const test = action.payload.payload.message.to
    myArray.filter((thread) => thread.property === test)

// OR:

case 'receive-message':
    const payload = action.payload.payload
    myArray.filter((thread) => thread.property === payload.message.to)

Playground link

  • Related