Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function "lacks return statement" but has typeguards for all the paths

Tags:

typescript

I have the following interfaces and types (all of which are open to change)

interface Base {
    type: string;
}

interface A extends Base {
    type: "A";
    field: string;
}

interface B extends Base {
    type: "B";
    query: string;
}

interface C extends Base {
    type: "C";
    equal: number;
}

type BaseExtensions = A | B | C;

interface BaseWrapper<R extends BaseExtensions> {
    idType: string;
    base: R;
}

interface And {
    type: "AND";
    rules: Array<BaseWrapper<any>>;
}

interface Or {
    type: "OR";
    rules: Array<BaseWrapper<any>>;
}

type Rule<R extends BaseExtensions> = BaseWrapper<R> | And | Or

What I would like to do is write the following function:

function doAThingBasedOnTheRuleType(rule: Rule<any>): Thing {
    if (isAnd(rule)) {
        return DoAndThing(rule);
    }
    if (isOr(rule)) {
        return DoOrThing(rule);
    }
    if (isA(rule.base)) {
        return DoAThing(rule);
    }
    if (isB(rule.base)) {
        return DoBThing(rule);
    }
    if (isC(rule.Base)) {
        return DoCThing(rule);
    }
    // error, [ts] Function lacks ending return statement and return type does not include 'undefined'.

}

I would expect that Rule starts out as And | Or | BaseWrapper<A> | BaseWrapper<B> | BaseWrapper<C> which should get narrowed down one by one. However, I get the error // error, [ts] Function lacks ending return statement and return type does not include 'undefined'.

1- Why is TS not able to infer the types? 2- How do I fix it?

I'm on TS 2.5.2

like image 518
marisbest2 Avatar asked Sep 26 '17 18:09

marisbest2


2 Answers

TypeScript's rule about implicit returns is enforced syntactically -- without knowing the types involved, there needs to be a return statement at all reachable exit points of the function.

To figure out that a given function's implicit return isn't reachable using the type system would require multiple "passes", which the type system currently doesn't do (for performance/complexity reasons).

For example, consider this code:

function fn1() {
    const f = fn2();
    if (f === "alpha") {
        return "A";
    } else if (f === "beta") {
        return "B";
    }
}

function fn2() {
    const f = fn1();
    if (f === "A") {
        return "alpha";
    } else if (f === "B") {
        return "beta";
    }
    return "gamma";
}

What's the type of fn1() ? It could be "A" | "B" | undefined if fn2() returns a value other than "alpha" or "beta", or it could be "A" | "B" if those are the only return values. Well, let's check fn2() -- what's its return type? That depends on the type of fn1() -- fn2 returns "alpha" | "beta" if fn1 returns only "A" | "B", or returns "alpha" | "beta" | "gamma" if undefined is a possible return value.

So to figure out the reachability of fn1's implicit return, you have to do multiple "rounds" of inference where you refine the type of one function based on the type of another, and then repeat, and hopefully you reach a fixed point and don't infinitely recurse. That's a lot more expensive than just doing a single pass with syntactic enforcement of implicit returns.

The easiest fix is to simply add a throw:

    } else if (...) {
       return ...;
    }
    throw new Error("Shouldn't be reachable");
}

Or if you're really into code coverage, rewrite the if condition to an assert in the final block:

   } else {
       Debug.assert(x.kind === "B");
       return "C";
   }
like image 161
Ryan Cavanaugh Avatar answered Sep 28 '22 07:09

Ryan Cavanaugh


If you're not going to throw any errors, and isC is your last rule check-point, couldn't you remove the condition (thus always returning the CThing at the end)?

function doAThingBasedOnTheRuleType(rule: Rule<any>): Thing {
    if (isAnd(rule)) {
        return DoAndThing(rule);
    }
    if (isOr(rule)) {
        return DoOrThing(rule);
    }
    if (isA(rule.base)) {
        return DoAThing(rule);
    }
    if (isB(rule.base)) {
        return DoBThing(rule);
    }

    return DoCThing(rule);
}
like image 27
Pedro Batista Avatar answered Sep 28 '22 07:09

Pedro Batista