Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

throw in arrow functions forces a void return type #7538

Closed
srijs opened this issue Mar 16, 2016 · 19 comments
Closed

throw in arrow functions forces a void return type #7538

srijs opened this issue Mar 16, 2016 · 19 comments
Assignees
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@srijs
Copy link

srijs commented Mar 16, 2016

TypeScript Version:

1.8

Code

// compiles
function foo(): number {
    throw new Error('Everything is gonna be alright!');
}

// does not compile
const bar: () => number = () => {
    throw new Error('WHYUNO?!?');
}

Expected behavior:
Functions foo and bar type-check, because an exception is thrown in their body which effectively means "bottom", so any return type should be permitted.

Actual behavior:
While function foo type-checks, bar does not.

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Mar 16, 2016
@mhegazy
Copy link
Contributor

mhegazy commented Mar 16, 2016

so is the proposal to disable the check if there is a contextual type?

@RyanCavanaugh
Copy link
Member

I suppose this is currently according-to-spec.

Options:

  • Functions with a single throw return any instead of void
  • Functions with a single throw get their contextual return type
  • Something else?

I don't think those are observably different

@mhegazy
Copy link
Contributor

mhegazy commented Mar 16, 2016

I suppose if all code path in a function result in a throw, the function could just return any

@srijs
Copy link
Author

srijs commented Mar 16, 2016

I lack the insight into the TS type inference engine to make a useful, concrete suggestion, but hand-wavingly, throw ... should behave similarly to return null for the purpose of return type inference.

This seems to work as expected for function so I was a actually surprised to see that => has different inference rules.

@RyanCavanaugh
Copy link
Member

The difference isn't between function and =>, it's about whether or not you have a return type annotation:

// OK
const bar: () => number = (): number => {
    throw new Error('WHYUNO?!?');
}

function foo() {
    throw new Error('Everything is gonna be alright!');
}
// Error
var f: () => number = foo;

@srijs
Copy link
Author

srijs commented Mar 17, 2016

Okay, I see. Still, I guess return null behaves like I would expect it to behave:

// OK
const bar: () => number = (): number => {
    return null;
}

function foo() {
    return null;
}
// OK
var f: () => number = foo;

Is there a chance we can get the same behaviour for throw?

@basarat
Copy link
Contributor

basarat commented Mar 17, 2016

As Ryan said, nothing special about arrow functions here. Here is another example:

// compiles
function foo(): number {
    throw new Error('Everything is gonna be alright!');
}

// compiles
const bar = (): number => {
    throw new Error('okay');
}

More

From #7538 (comment) For that code to compile one of two things can be done. I don't like either. Here are the reasons:

1.)

var retVoid: () => void;
var retNum: () => number = retsVoid; // Currently Error

I actually like this error and feel removing it is going to go in the inverse direction of safety.

2.)
So that leaves:

// Infers the return type as `null` which is the same as `any` in the current type system
const retAny = function() { return null; }
// Infers the return type as `void`
const retVoid = function() { throw new Error('err'); }

Again I actually prefer the inference of void here. I would argue that it should be void in the first case as well ... but at least its void in the second case :) 🌹

@srijs
Copy link
Author

srijs commented Mar 17, 2016

@basarat I agree that the first approach is not desirable.

For the second case though, why would you argue that throwing an error should be void? Returning nothing (void -- triviality) is not the same as not returning at all (<X>() => X -- impossibility). It's perfectly valid to implement a function <X>() => X using throw because there won't be a result at all, so the result can be of any type.

This principle is called ex falso quodlibet, meaning that if we deal with an impossibility (it's impossible to get a return value from a function that unconditionally throws), we can infer anything from it.

@srijs
Copy link
Author

srijs commented Mar 17, 2016

To reference some prior art from other programming languages that I would describe as having a reasonably sound type system:

Java (compiles):

public class HelloWorld{
     public int foo() {
         throw new RuntimeException();
     }
}

Haskell:

In Haskell the type of throw is:

throw :: forall a. Exception e => e -> a

Notice the universal quantification here! All return types are valid, since there is no return value.

Rust (compiles):

fn foo() -> i32 {
    panic!("OMG");
}

C++ (compiles):

int foo() {
   throw false;
}

@basarat
Copy link
Contributor

basarat commented Mar 17, 2016

@srijs Thanks for sharing that. I learnt something 🌹

For the examples:

fn foo() -> i32 {
    panic!("OMG");
}
int foo() {
   throw false;
}

TypeScript compiles as well:

function foo(): number {
    throw new Error('Everything is gonna be alright!');
}

But the question is what should it infer in the absence of a return type annotation.

PS : My opinions are my own. I might be wrong and am fine what whatever everyone else chooses. But don't mind me continuing to discuss this 🌹

@srijs
Copy link
Author

srijs commented Mar 17, 2016

Maybe I'm a little spoiled by Hindley-Milner, but I really like concept that type annotations don't change the type of an expression, but just aid the type checker in inferring types and can only ever narrow it down (so I can specialise <X>() => X to () => void) but don't change the result of the type inference (you can't generalise () => void to <X>() => X -- which seems a little like what's happening here).

I would like it to infer the most general type possible by default, and allow me to narrow it down rather than require me to add annotations to make it more general.

@basarat
Copy link
Contributor

basarat commented Mar 17, 2016

Consider:

function whatType(x : boolean) {
    if (x) {
        return false;
    } else {
        throw new Error("asdf");
    }
}

The inferred return type is boolean not any. So in the following case:

function whatType(x : boolean) {
    if (x) {

    } else {
        throw new Error("asdf");
    }
}

I feel the return type should be what it is the normal return path i.e. void. If there is a desire to have something else there one can always put an explicit return type and TypeScript will compile without complain 🌹

@basarat
Copy link
Contributor

basarat commented Mar 17, 2016

I discussed this more at work. The key difference from Haskell (I don't know that much but do know F#) is that JavaScript is not an expression oriented langauge. F# infers the type to be boolean as expected in this case:

let f x = if x then x else failwith "asdf";;

Basically there is an implicit return in any path to failwith. Having the universal quantification in failwith (and Exception in Haskell) means that it leaves the inference open for alternative code paths. TypeScript isn't as expression oriented. When one only has:

function whatType(x : boolean) {
    throw new Error("asdf");
}

It isn't

function whatType(x : boolean) {
    // WARNING: Compile error just as a proof of idea
    return throw new Error("asdf");
}

It is actually:

function whatType(x : boolean) {
    throw new Error("asdf");
    return void 0;
}

Hence the void return type (even though that return statement isn't actually reachable).

@srijs
Copy link
Author

srijs commented Mar 17, 2016

I think I brought up Java and C++ as well, which are probably more in the spirit of TypeScript (imperative, statement-oriented). I guess TypeScript is in an interesting spot here where it is one of probably very few imperative languages that have some form of type inference.

Don't get me wrong, I'm happy that I can achieve the universal quantification by adding additional type annotations. I was just pointing out that the compiler is inferring a less general signature which works in fewer cases and occasionally adds a bit of confusion (such as in my case).

Ideally I'd like to get away with as few explicit type annotations as possible (with noImplicitAny: true, though), and only annotate when I deliberately want to narrow down types, and TypeScript's inference gets in my way here.

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Bug A bug in TypeScript labels Apr 26, 2016
@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels May 16, 2016
@RyanCavanaugh
Copy link
Member

Basic idea for a neat fix here is that we can have a special no-return type that is the type of functions which don't have any reachable return points (either explicit return statements or implicit end-of-function-body returns). When computing the return type of a function, no-return is a no-op unless it's the only type, in which case the return type is no-return.

This enables a bunch of nice patterns -- you can e.g. return Debug.fail('...') when Debug#fail returns no-return and not pollute the return type of the containing function.

@ahejlsberg
Copy link
Member

I think we'll want to call it noreturn as you can't have dashes in identifiers. It would effectively be a "bottom" type that is assignable to anything and to which nothing is assignable.

@RyanCavanaugh
Copy link
Member

Ref #3076 in case there's some synergy there

@jeffreymorlan
Copy link
Contributor

Isn't this the same as the nothing type (empty union)?

@ahejlsberg
Copy link
Member

Fixed in #8652.

@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label May 24, 2016
@mhegazy mhegazy added this to the TypeScript 2.0 milestone May 24, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants