Skip to content

No implicit returns error in exhaustive if statement #17358

Closed
@OliverJAsh

Description

@OliverJAsh
Contributor

TS 2.4.1 with noImplicitReturns enabled

    class A {}
    class B {}

    // error: Not all code paths return a value.
    const fn = (x: A | B) => {
        if (x instanceof A) {
            x // A
            return 1
        } else if (x instanceof B) {
            x // B
            return 2
        } else {
            x // never
        }
    }

I would not expect this to error because the if statement is exhaustive, therefore all code paths do return a value.

Activity

j-oliveras

j-oliveras commented on Jul 22, 2017

@j-oliveras
Contributor

Is not exhaustive you can call fn function as fn({}) or fn({ x: 1 }). These calls are valid and are not A nor B.

So, I think, that works as intended.

OliverJAsh

OliverJAsh commented on Jul 22, 2017

@OliverJAsh
ContributorAuthor

@j-oliveras I think what you're saying makes sense, but I don't see how that applies to this example:

    interface A { type: 'A' }
    interface B { type: 'B' }

    // error: Not all code paths return a value.
    const fn = (x: A | B) => {
        if (x.type === 'A') {
            x // A
            return 1
        } else if (x.type === 'B') {
            x // B
            return 2
        } else {
            x // never
        }
    }

If using a switch statement instead of the if, the error goes. I would like to understand why we get this error when using if statements.

jcalz

jcalz commented on Jul 23, 2017

@jcalz
Contributor

@j-oliveras It is exhaustive as far as the type checker is concerned, since it infers a never type for x in the final else clause. Of course, as you note, you can fool it, due to an inconsistency between the nominal typing of instanceof at runtime and the structural typing of TypeScript (see, e.g., #11664). But that's not the issue here, as @OliverJAsh's followup demonstrates.

The simplest repro I can imagine is something like:

function foo(x: boolean) {
  if (x) return 1;
  if (!x) return 2;
  // x is inferred as never here, but ts still thinks some paths don't return a value
}

I'm guessing that the real issue is that the control flow analysis just isn't clever enough to realize that, once it has narrowed something to the never type, the surrounding code is unreachable and shouldn't count as a code path for the purposes of noImplicitReturns. Obviously there are workarounds (rework the if/else clauses to be obviously exhaustive to the compiler; throw an exception in any code you know is unreachable; etc.) but the question is whether the reported issue is a design limitation or a bug. I don't think it should be intended behavior.

DanielRosenwasser

DanielRosenwasser commented on Jul 24, 2017

@DanielRosenwasser
Member

I believe the problem is that only switch statements are special cased here. Basically, if the last statement in a function is a switch statement, we do an extra check for exhaustiveness, but we decided that if/elses might be too complex of a check. You'd have to check with @ahejlsberg for specifics.

You can always return the result of calling assertNever (documented here, available on npm here.

mhegazy

mhegazy commented on Aug 17, 2017

@mhegazy
Contributor

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

OliverJAsh

OliverJAsh commented on Oct 19, 2017

@OliverJAsh
ContributorAuthor

This also affects switch statements when they exist within an if statement. You can workaround it by wrapping the switch in an IIFE.

Is there anything TS can do to help here?

{
    enum Input { foo, bar }

    // Unexpected error: Not all code paths return a value.
    const fn = (input: Input) => {
        if ('foo') {
            switch (input) {
                case Input.bar: return 0;
                case Input.foo: return 1;
            }
        } else {
            return 1
        }
    }

    // Workaround: wrap switch in IIFE
    const fn2 = (input: Input) => {
        if ('foo') {
            return (() => {
                switch (input) {
                    case Input.bar: return 0;
                    case Input.foo: return 1;
                }
            })();
        } else {
            return 1
        }
    }
}
jcalz

jcalz commented on Oct 19, 2017

@jcalz
Contributor

@OliverJAsh said:

Is there anything TS can do to help here?

@DanielRosenwasser said:

You can always return the result of calling assertNever (documented here, available on npm here.)

OliverJAsh

OliverJAsh commented on Oct 19, 2017

@OliverJAsh
ContributorAuthor

@jcalz Thanks, that works.

TypeScript already special cases switch statements, as mentioned by @DanielRosenwasser, and so I was wondering if it could extend this behaviour to switch statements within if statements?

eucaos

eucaos commented on Nov 27, 2017

@eucaos

I have the same problem with nested switch clauses :
const reducer = (state: "A", action: "act1"): "A" => { switch (state) { case "A": switch (action) { case "act1": return state; } } };
I get function lacks ending return statement error message.

locked and limited conversation to collaborators on Jun 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @jcalz@OliverJAsh@DanielRosenwasser@eucaos@mhegazy

        Issue actions

          No implicit returns error in exhaustive if statement · Issue #17358 · microsoft/TypeScript