Home > Net >  TypeScript Discriminated Union with Optional Discriminant
TypeScript Discriminated Union with Optional Discriminant

Time:09-17

I've created a discriminated union that's later used to type props coming into a React component. A pared down sample case of what I've created looks like this:

type Client = {
    kind?: 'client',
    fn: (updatedIds: string[]) => void
};

type Server = {
    kind: 'server',
    fn: (selectionListId: string) => void
};

type Thing = Client | Server;

Note that the discriminant, kind, is optional in one code path, but is defaulted when it is destructured in the component definition:

function MyComponent(props: Thing) {
    const {
        kind = 'client',        
        fn
    } = props;

    if (kind === 'client') {
        props.fn(['hey']);
        // also an error:
        // fn(['hey'])
    } else {
        props.fn('hi')
        // also an error:
        // fn('hey')
    }
}

What I'm trying to understand is what's going on with this conditional. I understand that the type checker is having trouble properly narrowing the type of Thing, since the default value is separate from the type definition. The oddest part is that in both branches of the conditional it insists that fn is of type (arg0: string[] & string) => void and I don't understand why the type checker is trying to intersect the parameters here.

I would have expected it to be unhappy about non-exhaustiveness of the branches (i.e. not checking the undefined branch) or just an error on the else branch where the 'server' and undefined branches don't line up. But even trying to rearrange the code to be more explicit about each branch doesn't seem to make any impact.

Perhaps because the compiler simply can't narrow the types so tries an intersection so it doesn't matter which path--if the signatures are compatible then it's fine to call the function, otherwise you basically end up with a never (since string[] & string is an impossible type)?

I understand that there are a variety of ways I can resolve this via user-defined type predicates or type assertions, but I'm trying to get a better grasp on what's going on here internally and to find something a bit more elegant.

TS Playground link

CodePudding user response:

It's an implementation detail in TS. Types are not narrowed when you are storing the value in a different variable. The same issue exists for the square bracket notation. You can refer to this question, it deals with a similar issue. Apparently, this is done for compiler performance.

You can fix your issue by using both props.fn and props.kind.

playground

Or write a type guard function.

  • Related