Search code examples
typescripttype-inference

How to configure Typescript to infer discriminated unions when `is` and `never` is present?


I had a library in my project that I stopped using (deleted from package.json), and one of its peer dependencies was fp-ts, so I had to add fp-ts to my project. fp-ts has an Either type which can be checked for left/right values:

export declare const isLeft: <E>(ma: Either<E, unknown>) => ma is Left<E>

If I put this in an if

if (E.isLeft(result)) {
    // ...
}

then in the else Typescript will correctly infer that my Either has a right value.

My problem is that ever since I moved the dependency to my project (instead of indirectly using it as a peer dependency) the following case doesn't work anymore, I get a compiler error:

const fail = (msg: string): never => {
    throw new GenericProgramError(msg);
};

if (E.isLeft(result)) {
    fail("Expected a successful result");
}
expect(result.right).toEqual(
    //        ^^^--- Property 'right' does not exist on type 'Left'
    // ...
);

The problem here is that if result is a Left then I fail which returns never (throws), so Typescript should be able to infer that in expect result can only have a right and not a left. And this was working before. What do I need to change to fix this?


Solution

  • The answer is that your fail assertion function should be a function statement and not a fat-arrow function. (Thanks to david_p's answer here: https://stackoverflow.com/a/72689922/81723)

    So just change to function fail(...): never and it should work:

    function fail(msg: string): never {
        throw new GenericProgramError(msg);
    }
    

    Here's the full working example:

    import * as E from 'fp-ts/Either';
    declare function expect(value: any);
    
    const result = E.right<string, boolean>(true);
    
    function fail(msg: string): never {
      throw new Error(msg);
    }
    
    if (E.isLeft(result)) {
      fail("Expected a successful result");
    }
    
    expect(result.right).toEqual();
    //     ^^^ ✅ result: E.Right<boolean>
    

    Why this works

    I stumbled onto the answer in a roundabout way.

    I was trying to work this out by changing your code to an assertion function.

    But I got the strange error: "Assertions require every name in the call target to be declared with an explicit type annotation.(2775)"

    const fail2 = (result: E.Either<any, any>): asserts result is E.Right<any> => {
        if (E.isLeft(result)) {
            throw new Error('Expected a successful result');
        }
    };
        
    fail2(result);
    // Error: Assertions require every name in the call target to be declared with an explicit type annotation.(2775)
    

    Searching for this error gave me david_p's answer which explains that you can't use an arrow function as an assertion function (well, technically you can, but you need to explicitly define the type signature for the variable being assigned to. In practice it's easier to just use a function statement instead).