typescriptdiscriminated-uniondiscriminator

Handle discriminated union where discriminator is a union type


I am struggling with narrowing a complex discriminated union.

I used a bunch of if with early return to eliminate one by one all the case but at the end, I find myself with 2 default type lying on discriminator being union type and the narrowing stop working.

I have a simple snippet demonstrating my issue.

interface Foo {
  foo: string;
  type: 'foo';
}

interface BarBaz {
  bar: string;
  type: 'bar' | 'baz';
}

const func = function (arg: Foo | BarBaz) {
  if (arg.type === 'bar' || arg.type === 'baz') {
    return 'whatever';
  }

  arg; // type Foo or BarBaz expecting Foo
};

I guess, I should have only unique discriminator but is there a simple way to get out of this dead end?


Solution

  • This is considered a design limitation of TypeScript, reported at microsoft/TypeScript#31404. While the true branch of your check narrows as expected, TypeScript lacks the sort of counterfactual analysis necessary to do anything useful in the false branch.

    If you need this to work without refactoring to a "standard" discriminated union, you'll have to work around it. One way you can do it is to create a custom type guard function to discriminate the union explicitly:

    function discrimUnion<
      T extends Record<K, any>, 
      K extends PropertyKey, 
      const V extends T[K]
    >(t: T, k: K, ...v: V[]): t is Extract<T, Record<K, V>> {
        return v.includes(t[k]);
    }
    
    const func = function (arg: Foo | BarBaz) {
        if (discrimUnion(arg, "type", "bar", "baz")) {
            arg // BarBaz
            return 'whatever';
        }
        arg; // Foo
        return;
    };
    

    Or you could change to a switch/case statement which seems to be better about determining exhaustiveness:

    const func = function (arg: Foo | BarBaz) {
        switch (arg.type) {
            case "bar":
            case "baz": {
                arg; // BarBaz
                return 'whatever'
            }
            default: {
                arg // Foo
                return;
            }
        }
    }
    

    Playground link to code