You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
throw in arrow functions forces a void return type #7538
// compilesfunctionfoo(): number{thrownewError('Everything is gonna be alright!');}// does not compileconstbar: ()=>number=()=>{thrownewError('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.
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.
The difference isn't between function and =>, it's about whether or not you have a return type annotation:
// OKconstbar: ()=>number=(): number=>{thrownewError('WHYUNO?!?');}functionfoo(){thrownewError('Everything is gonna be alright!');}// Errorvarf: ()=>number=foo;
As Ryan said, nothing special about arrow functions here. Here is another example:
// compilesfunctionfoo(): number{thrownewError('Everything is gonna be alright!');}// compilesconstbar=(): number=>{thrownewError('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.)
varretVoid: ()=>void;varretNum: ()=>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 systemconstretAny=function(){returnnull;}// Infers the return type as `void`constretVoid=function(){thrownewError('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 :) 🌹
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.
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.
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 🌹
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:
letf 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:
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.
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.
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.
Activity
mhegazy commentedon Mar 16, 2016
so is the proposal to disable the check if there is a contextual type?
RyanCavanaugh commentedon Mar 16, 2016
I suppose this is currently according-to-spec.
Options:
throwreturnanyinstead ofvoidthrowget their contextual return typeI don't think those are observably different
mhegazy commentedon Mar 16, 2016
I suppose if all code path in a function result in a throw, the function could just return
anysrijs commentedon 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 toreturn nullfor the purpose of return type inference.This seems to work as expected for
functionso I was a actually surprised to see that=>has different inference rules.RyanCavanaugh commentedon Mar 16, 2016
The difference isn't between
functionand=>, it's about whether or not you have a return type annotation:srijs commentedon Mar 17, 2016
Okay, I see. Still, I guess
return nullbehaves like I would expect it to behave:Is there a chance we can get the same behaviour for
throw?basarat commentedon Mar 17, 2016
As Ryan said, nothing special about arrow functions here. Here is another example:
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.)
I actually like this error and feel removing it is going to go in the inverse direction of safety.
2.)
So that leaves:
Again I actually prefer the inference of
voidhere. I would argue that it should bevoidin the first case as well ... but at least itsvoidin the second case :) 🌹srijs commentedon Mar 17, 2016
Basarat Ali Syed (@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>() => Xusingthrowbecause 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 commentedon 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):
Haskell:
In Haskell the type of
throwis:Notice the universal quantification here! All return types are valid, since there is no return value.
Rust (compiles):
C++ (compiles):
basarat commentedon Mar 17, 2016
Sam Reis (@srijs) Thanks for sharing that. I learnt something 🌹
For the examples:
TypeScript compiles as well:
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 commentedon 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>() => Xto() => void) but don't change the result of the type inference (you can't generalise() => voidto<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 commentedon Mar 17, 2016
Consider:
The inferred return type is
booleannotany. So in the following case: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 commentedon 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
booleanas expected in this case:Basically there is an implicit
returnin any path tofailwith. Having the universal quantification infailwith(andExceptionin Haskell) means that it leaves the inference open for alternative code paths. TypeScript isn't as expression oriented. When one only has:It isn't
It is actually:
Hence the
voidreturn type (even though thatreturnstatement isn't actually reachable).srijs commentedon 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.RyanCavanaugh commentedon May 16, 2016
Basic idea for a neat fix here is that we can have a special
no-returntype that is the type of functions which don't have any reachable return points (either explicitreturnstatements or implicit end-of-function-body returns). When computing the return type of a function,no-returnis a no-op unless it's the only type, in which case the return type isno-return.This enables a bunch of nice patterns -- you can e.g.
return Debug.fail('...')whenDebug#failreturnsno-returnand not pollute the return type of the containing function.ahejlsberg commentedon May 16, 2016
I think we'll want to call it
noreturnas 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 commentedon May 16, 2016
Ref #3076 in case there's some synergy there
jeffreymorlan commentedon May 16, 2016
Isn't this the same as the
nothingtype (empty union)?ahejlsberg commentedon May 24, 2016
Fixed in #8652.