Typescript not inferring types in switch statement when generics and conditionals are used in the type definition

Viewed 1320

I have a type definition for a function. I'm defining the type of the second argument as conditional on the type of the first argument, like so:

const FOO = "FOO";
const BAR = "BAR";

let fooPayload = {
    zip: "zap"
}
let barPayload = {
    cat: "dog"
}

type ActionTypes = typeof FOO | typeof BAR;
interface MyFunction<T extends ActionTypes = ActionTypes> {
  (
    action: {
      type: T;
      payload: T extends typeof FOO
        ? typeof fooPayload
        : typeof barPayload;
    }
  ): boolean;
}

The function that MyFunction interface refers to has a switch statement within it that switches based on action.type and, depending on the case, does something with action.payload. Here's an example:

const myFunction:MyFunction = (action) => {
    switch (action.type) {
        case FOO:
            action.payload.zip = "new zap"
            return true
        case BAR: 
            action.payload.cat = "new dog"
            return false
        default:
            return false
    }
}

The issue I'm having is that Typescript isn't correctly inferring what action.payload should be from the switch statement. For example, if action.type is equal to "FOO" then it should infer that action.payload MUST be typeof fooPayload. Instead, it infers that it is typeof fooPayload | typeof barPayload.

This doesn't make any sense as the type definition for action.payload is based on the value of action.type, and since action.payload.zip is being called within a switch statement that can only occur when action.type equals "FOO", it should infer that the only possible type for action.payload is typeof fooPayload.

Here's a code example

What am I doing wrong here?

1 Answers

I'm not sure of the exact place this is causing issues but in general the problem is you are trying to reinvent what typescript already does well, narrow type unions.

Conditionals are a really interesting feature but behave a bit unexpectedly sometimes as they leak out of the scope of where they are defined since they are distributive.

I would implement this a different way:

interface Action<T, P> {
    type: T,
    payload: P
}

type Actions 
    = Action<typeof FOO, typeof FooPayload> 
    | Action<typeof BAR, typeof BarPayload> 
    ;

interface MyFunction {
  (action: Actions): boolean;
}

No conditionals needed, and typescript will narrow the unions as expected in the switch.

Related